├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── manifest.json └── og.png └── src ├── App.css ├── App.js ├── ComponentGrid.js ├── Directory.js ├── DynamicLighting ├── DynamicLighting.js ├── LightCanvas.js ├── lightToStyles.js ├── useDimensions.js ├── useMousePosition.js ├── useRayCasterEngine.js └── vector.js ├── FlagObjectBuilder ├── Controls.js ├── FlagObject.js ├── FlagObjectBuilder.js ├── components.js └── theme.js ├── GenerativeLayout ├── Article.js ├── GenerativeGrid.js ├── GeneratorContainer.js ├── GeneratorControls.js ├── generative-layout.css ├── index.js ├── useDebugGrid.js └── utils.js ├── GenerativePalette ├── DynamicCanvas.js ├── GenerativePalette.js ├── Sliders.js └── useGenerativePalette.js ├── Info.js ├── Pager.js ├── RadioGroup.js ├── Responsive ├── Artboard.js ├── RedLines.js ├── Responsive.js └── components.js ├── Toggle.js ├── components.js ├── icons ├── arrow-down.svg ├── arrow-left.svg ├── arrow-right.svg ├── arrow-up.svg ├── flag.svg ├── info.svg ├── layout.svg ├── maximize-2.svg ├── menu.svg ├── search.svg ├── sliders.svg ├── sun.svg └── x-circle.svg ├── imgs ├── ayla.jpg ├── ernest-porzi-19106-unsplash.jpg ├── fabian-moller-401625-unsplash.jpg ├── light.png ├── light.svg ├── paridhi.png ├── sandra-ollier-663002-unsplash.jpg └── zainab.png ├── index.css ├── index.js ├── primitives.js ├── serviceWorker.js ├── theme.js └── useThemes.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic-lighting", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/core": "^10.0.10", 7 | "@emotion/styled": "^10.0.11", 8 | "@reach/dialog": "^0.2.8", 9 | "@reach/router": "^1.2.1", 10 | "@rehooks/component-size": "^1.0.2", 11 | "@styled-system/should-forward-prop": "^5.0.12", 12 | "@styled-system/theme-get": "^5.0.18", 13 | "canvas-sketch-util": "^1.8.0", 14 | "chroma-js": "^2.0.3", 15 | "constate": "^1.2.0", 16 | "emotion-theming": "^10.0.10", 17 | "hsluv": "0.0.3", 18 | "rc-slider": "^8.6.13", 19 | "re-resizable": "^5.0.1", 20 | "react": "^16.8.6", 21 | "react-dom": "^16.8.6", 22 | "react-pose": "^4.0.8", 23 | "react-scripts": "3.0.1", 24 | "rebass": "^3.1.1", 25 | "resize-observer-polyfill": "^1.5.1", 26 | "styled-system": "^5.0.12", 27 | "tachyons": "^4.11.1" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "react-scripts test", 33 | "eject": "react-scripts eject" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Generative.parts 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 46 | React App 47 | 48 | 49 | 50 |
51 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/public/og.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | body { 4 | background-color: black; 5 | cursor: move; 6 | } 7 | 8 | [data-reach-dialog-overlay] { 9 | z-index: 999; 10 | } 11 | 12 | :root { 13 | --material-white: #fff; 14 | --material-gray: #898ba3; 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router } from '@reach/router'; 3 | import { DynamicLighting } from './DynamicLighting/DynamicLighting'; 4 | import { GenerativePalette } from './GenerativePalette/GenerativePalette'; 5 | import { Responsive } from './Responsive/Responsive'; 6 | import { Directory } from './Directory'; 7 | import { GenerativeLayout } from './GenerativeLayout'; 8 | import { FlagObjectBuilder } from './FlagObjectBuilder/FlagObjectBuilder'; 9 | import './App.css'; 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/ComponentGrid.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { grid } from 'styled-system'; 3 | import { Card, Flex } from './primitives'; 4 | 5 | export const ComponentGrid = styled(Card)(grid, { 6 | position: 'relative', 7 | }); 8 | 9 | ComponentGrid.defaultProps = { 10 | backgroundColor: 'tertiary', 11 | alignContent: ['flex-end', 'center'], 12 | justifyItems: 'center', 13 | justifyContent: 'center', 14 | borderRadius: [0, 0, 0, 2], 15 | flexWrap: 'wrap', 16 | height: ['100%', '100%', '100%', 720], 17 | width: ['100%', '100%', '100%', 1184], 18 | }; 19 | 20 | ComponentGrid.One = styled(Flex)({ zIndex: 1 }); 21 | ComponentGrid.One.defaultProps = { 22 | flexDirection: 'column', 23 | mr: [2, 3, 3], 24 | ml: [2, 0, 0], 25 | mb: [3, 0, 0], 26 | width: ['auto', 240, 240], 27 | flex: ['1 0 192px', 'none', 'none'], 28 | }; 29 | 30 | ComponentGrid.Two = styled(Flex)({ zIndex: 1 }); 31 | ComponentGrid.Two.defaultProps = { 32 | flexDirection: 'column', 33 | width: ['100%', 384, 384], 34 | mr: [2, 0, 3], 35 | ml: [2, 0, 0], 36 | }; 37 | 38 | ComponentGrid.Three = styled(Flex)({ zIndex: 1 }); 39 | ComponentGrid.Three.defaultProps = { 40 | flexDirection: ['row', 'row', 'column'], 41 | width: ['100%', 640, 176], 42 | height: [128, 176, 472], 43 | mt: [3, 3, 0], 44 | mx: [2, 0, 0], 45 | }; 46 | -------------------------------------------------------------------------------- /src/Directory.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'emotion-theming'; 3 | import { Link as RouterLink } from '@reach/router'; 4 | import styled from '@emotion/styled'; 5 | import { border, shadow, background, grid } from 'styled-system'; 6 | import shouldForwardProp from '@styled-system/should-forward-prop'; 7 | import theme from './theme'; 8 | import { Flex, Link, Heading, Box, Icon } from './primitives'; 9 | import { ReactComponent as SunIcon } from './icons/sun.svg'; 10 | import { ReactComponent as SlidersIcon } from './icons/sliders.svg'; 11 | import { ReactComponent as MaxIcon } from './icons/maximize-2.svg'; 12 | import { ReactComponent as LayoutIcon } from './icons/layout.svg'; 13 | import { ReactComponent as FlagIcon } from './icons/flag.svg'; 14 | 15 | const ExperimentLink = styled(Link, { shouldForwardProp })( 16 | { display: 'flex' }, 17 | props => ({ 18 | ':hover, :focus': { 19 | backgroundColor: props.theme.colors.primary, 20 | color: props.theme.colors.black, 21 | }, 22 | }), 23 | border, 24 | shadow, 25 | background, 26 | grid, 27 | ); 28 | ExperimentLink.defaultProps = { 29 | width: [4, 5], 30 | height: [4, 5], 31 | backgroundColor: 'white', 32 | borderRadius: '3px', 33 | alignItems: 'center', 34 | justifyContent: 'center', 35 | color: 'black', 36 | mr: 3, 37 | mb: 3, 38 | }; 39 | 40 | export const Directory = () => ( 41 | 42 | 43 | 44 | Generative.parts 45 | 46 | 47 | 48 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | 93 | 94 | 95 | ); 96 | -------------------------------------------------------------------------------- /src/DynamicLighting/DynamicLighting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'emotion-theming'; 3 | import theme from '../theme'; 4 | import { Container } from '../primitives'; 5 | import { useThemes } from '../useThemes'; 6 | import LightCanvas from './LightCanvas'; 7 | 8 | export function DynamicLighting() { 9 | const [data, activeIndex, selectTheme] = useThemes(); 10 | 11 | return ( 12 | 13 | 14 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicLighting/LightCanvas.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { withTheme } from 'emotion-theming'; 3 | import { 4 | TypographySwatch, 5 | ColorSwatch, 6 | NavBar, 7 | MediaCard, 8 | SearchBar, 9 | ProfileCard, 10 | } from '../components'; 11 | import { ComponentGrid } from '../ComponentGrid'; 12 | import { Pager } from '../Pager'; 13 | import { InfoButton } from '../Info'; 14 | import { Toggle } from '../Toggle'; 15 | import { useDimensions } from './useDimensions'; 16 | import { useRayCasterEngine, RayCasterDebug } from './useRayCasterEngine'; 17 | import { lightToStyles } from './lightToStyles'; 18 | 19 | function LightCanvas({ profile, media, activeIndex, selectTheme, theme }) { 20 | const typeSwRef = useRef(null); 21 | const profileRef = useRef(null); 22 | const navRef = useRef(null); 23 | const mediaRef = useRef(null); 24 | const searchRef = useRef(null); 25 | const blackRef = useRef(null); 26 | const primaryRef = useRef(null); 27 | const secondaryRef = useRef(null); 28 | const canvasRef = useRef(null); 29 | 30 | const [windowDims, surfaceDims] = useDimensions( 31 | typeSwRef, 32 | profileRef, 33 | navRef, 34 | mediaRef, 35 | searchRef, 36 | blackRef, 37 | primaryRef, 38 | secondaryRef, 39 | ); 40 | 41 | const light = useRayCasterEngine( 42 | windowDims, 43 | surfaceDims, 44 | canvasRef, 45 | theme.colors.primary, 46 | ); 47 | 48 | const [debug, setDebug] = useState(false); 49 | 50 | return ( 51 | 52 | {debug && } 53 | setDebug(!debug)} 61 | /> 62 | 63 | 64 | 65 | 70 | 76 | 77 | 78 | 79 | 84 | 91 | 96 | 97 | 98 | 99 | 107 | 115 | 121 | 122 | 123 | 124 | 125 | ); 126 | } 127 | 128 | export default withTheme(LightCanvas); 129 | -------------------------------------------------------------------------------- /src/DynamicLighting/lightToStyles.js: -------------------------------------------------------------------------------- 1 | import chroma from 'chroma-js'; 2 | 3 | export function lightToStyles(light = {}, colors) { 4 | const { volume = 0 } = light; 5 | 6 | return { 7 | // '--material-white': chroma 8 | // .mix('white', colors.tertiary, mapRange(lightVolume, 0, 1, 0.2, 1), 'hsl') 9 | // .css(), 10 | // '--material-white': chroma(colors.tertiary) 11 | // .luminance(mapRange(lightVolume, 0, 1, 0.5, 1)) 12 | // .css(), 13 | transitionProperty: 'color, background-color', 14 | transitionDuration: '0.15s', 15 | transitionTimingFunction: 'ease-in-out', 16 | '--material-white': chroma 17 | .scale([colors.tertiary, colors.materialWhite]) 18 | .domain([0, 0.25, 1])(mapRange(volume, 0, 1, 0, 1)) 19 | .css(), 20 | '--material-gray': chroma(colors.materialGray) 21 | .darken(mapRange(volume, 0, 1, 2, 0)) 22 | .css(), 23 | boxShadow: shadow(light), 24 | }; 25 | } 26 | 27 | function shadow({ volume = 0, direction = [0, 0], distance = 0 }) { 28 | const [x, y] = direction; 29 | const xOff = shadowOffset(x); 30 | const yOff = shadowOffset(y); 31 | const opacity = mapRange(volume, 0, 1, 0, 0.2); 32 | 33 | return `${xOff}px ${yOff}px 8px 0 rgba(0, 0, 0, ${opacity})`; 34 | } 35 | 36 | function shadowOffset(v) { 37 | return mapRange(v, -1, 1, 8, -8).toFixed(3); 38 | } 39 | 40 | function constrain(n, low, high) { 41 | return Math.max(Math.min(n, high), low); 42 | } 43 | 44 | function mapRange(n, start1, stop1, start2, stop2) { 45 | const value = ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2; 46 | if (start2 < stop2) { 47 | return constrain(value, start2, stop2); 48 | } else { 49 | return constrain(value, stop2, start2); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DynamicLighting/useDimensions.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState, useEffect } from 'react'; 2 | 3 | export function useDimensions( 4 | typeSwRef, 5 | profileRef, 6 | navRef, 7 | mediaRef, 8 | searchRef, 9 | blackRef, 10 | primaryRef, 11 | secondaryRef, 12 | ) { 13 | const [dimensions, setDimensions] = useState([{}, {}]); 14 | 15 | const handleResize = useCallback(() => { 16 | const size = [ 17 | { 18 | width: window.innerWidth, 19 | height: window.innerHeight, 20 | }, 21 | { 22 | typeSw: typeSwRef.current 23 | ? typeSwRef.current.getBoundingClientRect() 24 | : null, 25 | profile: profileRef.current 26 | ? profileRef.current.getBoundingClientRect() 27 | : null, 28 | nav: navRef.current ? navRef.current.getBoundingClientRect() : null, 29 | media: mediaRef.current 30 | ? mediaRef.current.getBoundingClientRect() 31 | : null, 32 | search: searchRef.current 33 | ? searchRef.current.getBoundingClientRect() 34 | : null, 35 | black: blackRef.current 36 | ? blackRef.current.getBoundingClientRect() 37 | : null, 38 | primary: primaryRef.current 39 | ? primaryRef.current.getBoundingClientRect() 40 | : null, 41 | secondary: secondaryRef.current 42 | ? secondaryRef.current.getBoundingClientRect() 43 | : null, 44 | }, 45 | ]; 46 | 47 | setDimensions(size); 48 | }, [ 49 | typeSwRef, 50 | profileRef, 51 | navRef, 52 | mediaRef, 53 | searchRef, 54 | blackRef, 55 | primaryRef, 56 | secondaryRef, 57 | ]); 58 | 59 | useEffect(() => { 60 | if (!typeSwRef.current) { 61 | return; 62 | } 63 | 64 | handleResize(); 65 | 66 | window.addEventListener('resize', handleResize); 67 | return () => window.removeEventListener('resize', handleResize); 68 | // eslint-disable-next-line 69 | }, [ 70 | typeSwRef.current, 71 | profileRef.current, 72 | navRef.current, 73 | mediaRef.current, 74 | searchRef.current, 75 | blackRef.current, 76 | primaryRef.current, 77 | secondaryRef.current, 78 | ]); 79 | 80 | return dimensions; 81 | } 82 | -------------------------------------------------------------------------------- /src/DynamicLighting/useMousePosition.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * from https://github.com/rehooks/window-mouse-position/blob/master/index.js 5 | */ 6 | export function useWindowMousePosition() { 7 | let [WindowMousePosition, setWindowMousePosition] = useState({ 8 | x: null, 9 | y: null, 10 | }); 11 | 12 | function handleMouseMove(e) { 13 | setWindowMousePosition({ 14 | x: e.pageX, 15 | y: e.pageY, 16 | }); 17 | } 18 | 19 | useEffect(() => { 20 | window.addEventListener('mousemove', handleMouseMove); 21 | 22 | return () => { 23 | window.removeEventListener('mousemove', handleMouseMove); 24 | }; 25 | }, []); 26 | 27 | return WindowMousePosition; 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicLighting/useRayCasterEngine.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import styled from '@emotion/styled'; 3 | import Vector, { createVector, toRadians } from './vector'; 4 | import { useWindowMousePosition } from './useMousePosition'; 5 | 6 | export const RayCasterDebug = styled.canvas` 7 | position: fixed; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | z-index: 0; 15 | background-color: transparent; 16 | `; 17 | 18 | /** 19 | * Boundary 20 | * Surfaces that interact with light rays 21 | */ 22 | function createBoundary([x1, y1], [x2, y2], type) { 23 | const a = createVector(x1, y1); 24 | const b = createVector(x2, y2); 25 | 26 | return { 27 | a, 28 | b, 29 | type, 30 | draw(context) { 31 | context.lineWidth = 2; 32 | context.beginPath(); 33 | context.moveTo(a.x, a.y); 34 | context.lineTo(b.x, b.y); 35 | context.stroke(); 36 | }, 37 | }; 38 | } 39 | 40 | /** 41 | * Light Ray 42 | */ 43 | function createRay(position, angle, limit) { 44 | const dir = Vector.fromAngle(angle); 45 | 46 | return { 47 | lookAt(x, y) { 48 | dir.x = x - position.x; 49 | dir.y = y - position.y; 50 | dir.normalize(); 51 | }, 52 | cast(surface) { 53 | const x1 = surface.a.x; 54 | const y1 = surface.a.y; 55 | const x2 = surface.b.x; 56 | const y2 = surface.b.y; 57 | 58 | const x3 = position.x; 59 | const y3 = position.y; 60 | const x4 = position.x + dir.x; 61 | const y4 = position.y + dir.y; 62 | 63 | const den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); 64 | if (den === 0) { 65 | return; 66 | } 67 | 68 | const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / den; 69 | const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / den; 70 | if (t > 0 && t < 1 && u > 0) { 71 | const pt = createVector(); 72 | pt.x = x1 + t * (x2 - x1); 73 | pt.y = y1 + t * (y2 - y1); 74 | return pt; 75 | } else { 76 | return undefined; 77 | } 78 | }, 79 | draw(context) { 80 | context.beginPath(); 81 | context.moveTo(position.x, position.y); 82 | context.lineTo(position.x + dir.x * limit, position.y + dir.y * limit); 83 | context.stroke(); 84 | }, 85 | }; 86 | } 87 | 88 | /** 89 | * Light Source 90 | * Generates rays in 360° and then casts 91 | * them onto the surfaces 92 | */ 93 | function createLight(x, y, limit) { 94 | const position = createVector(x, y); 95 | 96 | const rays = [...Array(360).keys()].map((_, a) => 97 | createRay(position, toRadians(a), limit), 98 | ); 99 | 100 | return { 101 | position, 102 | rays, 103 | draw(context) { 104 | context.lineWidth = 1; 105 | context.beginPath(); 106 | context.ellipse(position.x, position.y, 5, 5, 0, 0, 2 * Math.PI); 107 | context.fill(); 108 | }, 109 | }; 110 | } 111 | 112 | function look({ rays, position }, surfaces) { 113 | return rays.reduce((acc, ray) => { 114 | let closest = null; 115 | let closestSurface = null; 116 | let record = Infinity; 117 | 118 | surfaces.forEach(surface => { 119 | const pt = ray.cast(surface); 120 | 121 | if (pt) { 122 | const d = position.dist(pt); 123 | 124 | if (d < record) { 125 | record = d; 126 | closest = pt; 127 | closestSurface = surface; 128 | } 129 | } 130 | }); 131 | 132 | return closest 133 | ? acc.concat([ 134 | { 135 | a: [position.x, position.y], 136 | b: [closest.x, closest.y], 137 | type: closestSurface.type, 138 | }, 139 | ]) 140 | : acc; 141 | }, []); 142 | } 143 | 144 | export function useRayCasterEngine( 145 | { width = 0, height = 0 }, 146 | surfaceDims, 147 | canvasRef, 148 | color = 'white', 149 | ) { 150 | const [lights, setLights] = useState({}); 151 | let { x, y } = useWindowMousePosition(); 152 | 153 | useEffect(() => { 154 | const light = createLight(x, y, Math.max(width, height)); 155 | const p = 0; // padding 156 | 157 | const surfaces = Object.keys(surfaceDims).map(name => ({ 158 | name, 159 | position: createVector( 160 | surfaceDims[name].left + surfaceDims[name].width / 2, 161 | surfaceDims[name].top + surfaceDims[name].height / 2, 162 | ), 163 | dims: surfaceDims[name], 164 | })); 165 | 166 | const boundaries = surfaces 167 | .filter(s => s.dims) 168 | .map(({ dims: { top: t, left: l, bottom: b, right: r } = {}, name }) => [ 169 | createBoundary([l - p, t - p], [r + p, t - p], name), 170 | createBoundary([r + p, t - p], [r + p, b + p], name), 171 | createBoundary([r + p, b + p], [l - p, b + p], name), 172 | createBoundary([l - p, b + p], [l - p, t - p], name), 173 | ]) 174 | .flat(); 175 | 176 | const interactions = look(light, boundaries); 177 | 178 | const lightVolumes = interactions.reduce((acc, ray) => { 179 | if (!acc[ray.type]) acc[ray.type] = 0; 180 | acc[ray.type] = acc[ray.type] < 1 ? acc[ray.type] + 0.1 : acc[ray.type]; 181 | return acc; 182 | }, {}); 183 | 184 | setLights( 185 | surfaces.reduce((acc, surface) => { 186 | const distance = light.position.dist(surface.position); 187 | 188 | acc[surface.name] = { 189 | direction: light.position 190 | .copy() 191 | .sub(surface.position) 192 | .normalize() 193 | .array(), 194 | distance, 195 | volume: lightVolumes[surface.name], 196 | }; 197 | 198 | return acc; 199 | }, {}), 200 | ); 201 | 202 | if (canvasRef.current) { 203 | canvasRef.current.width = width * 2; 204 | canvasRef.current.height = height * 2; 205 | canvasRef.current.style.width = `${width}px`; 206 | canvasRef.current.style.height = `${height}px`; 207 | 208 | const context = canvasRef.current.getContext('2d'); 209 | context.scale(2, 2); 210 | context.clearRect(0, 0, width, height); 211 | 212 | context.strokeStyle = 'rgba(255, 255, 255, 0.25)'; 213 | // context.fillStyle = color; 214 | 215 | if (boundaries) { 216 | // boundaries.forEach(boundary => boundary.draw(context)); 217 | // light.draw(context); 218 | 219 | interactions.forEach(({ a, b }) => { 220 | context.beginPath(); 221 | context.moveTo(...a); 222 | context.lineTo(...b); 223 | context.stroke(); 224 | }); 225 | } 226 | } 227 | }, [width, height, surfaceDims, x, y, canvasRef, color]); 228 | 229 | return lights; 230 | } 231 | -------------------------------------------------------------------------------- /src/DynamicLighting/vector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * P5 Vector 3 | * from: 4 | * https://github.com/processing/p5.js/blob/0.8.0/src/math/p5.Vector.js#L12 5 | */ 6 | export default function Vector(x, y, z = 0) { 7 | this.x = x; 8 | this.y = y; 9 | this.z = z; 10 | } 11 | 12 | Vector.prototype.toString = function p5VectorToString() { 13 | return 'Vector Object : [' + this.x + ', ' + this.y + ', ' + this.z + ']'; 14 | }; 15 | 16 | Vector.prototype.set = function set(x, y, z) { 17 | if (x instanceof Vector) { 18 | this.x = x.x || 0; 19 | this.y = x.y || 0; 20 | this.z = x.z || 0; 21 | return this; 22 | } 23 | if (x instanceof Array) { 24 | this.x = x[0] || 0; 25 | this.y = x[1] || 0; 26 | this.z = x[2] || 0; 27 | return this; 28 | } 29 | this.x = x || 0; 30 | this.y = y || 0; 31 | this.z = z || 0; 32 | return this; 33 | }; 34 | 35 | Vector.prototype.copy = function copy() { 36 | return new Vector(this.x, this.y, this.z); 37 | }; 38 | 39 | Vector.prototype.add = function add(x, y, z) { 40 | if (x instanceof Vector) { 41 | this.x += x.x || 0; 42 | this.y += x.y || 0; 43 | this.z += x.z || 0; 44 | return this; 45 | } 46 | if (x instanceof Array) { 47 | this.x += x[0] || 0; 48 | this.y += x[1] || 0; 49 | this.z += x[2] || 0; 50 | return this; 51 | } 52 | this.x += x || 0; 53 | this.y += y || 0; 54 | this.z += z || 0; 55 | return this; 56 | }; 57 | 58 | Vector.prototype.sub = function sub(x, y, z) { 59 | if (x instanceof Vector) { 60 | this.x -= x.x || 0; 61 | this.y -= x.y || 0; 62 | this.z -= x.z || 0; 63 | return this; 64 | } 65 | if (x instanceof Array) { 66 | this.x -= x[0] || 0; 67 | this.y -= x[1] || 0; 68 | this.z -= x[2] || 0; 69 | return this; 70 | } 71 | this.x -= x || 0; 72 | this.y -= y || 0; 73 | this.z -= z || 0; 74 | return this; 75 | }; 76 | 77 | Vector.prototype.mult = function mult(n) { 78 | if (!(typeof n === 'number' && isFinite(n))) { 79 | console.warn( 80 | 'Vector.prototype.mult:', 81 | 'n is undefined or not a finite number', 82 | ); 83 | return this; 84 | } 85 | this.x *= n; 86 | this.y *= n; 87 | this.z *= n; 88 | return this; 89 | }; 90 | 91 | Vector.prototype.div = function div(n) { 92 | if (!(typeof n === 'number' && isFinite(n))) { 93 | console.warn( 94 | 'Vector.prototype.div:', 95 | 'n is undefined or not a finite number', 96 | ); 97 | return this; 98 | } 99 | if (n === 0) { 100 | console.warn('Vector.prototype.div:', 'divide by 0'); 101 | return this; 102 | } 103 | this.x /= n; 104 | this.y /= n; 105 | this.z /= n; 106 | return this; 107 | }; 108 | 109 | Vector.prototype.mag = function mag() { 110 | return Math.sqrt(this.magSq()); 111 | }; 112 | 113 | Vector.prototype.magSq = function magSq() { 114 | var x = this.x; 115 | var y = this.y; 116 | var z = this.z; 117 | return x * x + y * y + z * z; 118 | }; 119 | 120 | Vector.prototype.dot = function dot(x, y, z) { 121 | if (x instanceof Vector) { 122 | return this.dot(x.x, x.y, x.z); 123 | } 124 | return this.x * (x || 0) + this.y * (y || 0) + this.z * (z || 0); 125 | }; 126 | 127 | Vector.prototype.cross = function cross(v) { 128 | var x = this.y * v.z - this.z * v.y; 129 | var y = this.z * v.x - this.x * v.z; 130 | var z = this.x * v.y - this.y * v.x; 131 | return new Vector(x, y, z); 132 | }; 133 | 134 | Vector.prototype.dist = function dist(v) { 135 | return v 136 | .copy() 137 | .sub(this) 138 | .mag(); 139 | }; 140 | 141 | Vector.prototype.normalize = function normalize() { 142 | var len = this.mag(); 143 | // here we multiply by the reciprocal instead of calling 'div()' 144 | // since div duplicates this zero check. 145 | if (len !== 0) this.mult(1 / len); 146 | return this; 147 | }; 148 | 149 | Vector.prototype.limit = function limit(max) { 150 | var mSq = this.magSq(); 151 | if (mSq > max * max) { 152 | this.div(Math.sqrt(mSq)) //normalize it 153 | .mult(max); 154 | } 155 | return this; 156 | }; 157 | 158 | Vector.prototype.setMag = function setMag(n) { 159 | return this.normalize().mult(n); 160 | }; 161 | 162 | Vector.prototype.heading = function heading() { 163 | var h = Math.atan2(this.y, this.x); 164 | return fromRadians(h); 165 | }; 166 | 167 | Vector.prototype.rotate = function rotate(a) { 168 | var newHeading = this.heading() + a; 169 | newHeading = toRadians(newHeading); 170 | var mag = this.mag(); 171 | this.x = Math.cos(newHeading) * mag; 172 | this.y = Math.sin(newHeading) * mag; 173 | return this; 174 | }; 175 | 176 | Vector.prototype.angleBetween = function angleBetween(v) { 177 | var dotmagmag = this.dot(v) / (this.mag() * v.mag()); 178 | // Mathematically speaking: the dotmagmag variable will be between -1 and 1 179 | // inclusive. Practically though it could be slightly outside this range due 180 | // to floating-point rounding issues. This can make Math.acos return NaN. 181 | // 182 | // Solution: we'll clamp the value to the -1,1 range 183 | var angle = Math.acos(Math.min(1, Math.max(-1, dotmagmag))); 184 | return fromRadians(angle); 185 | }; 186 | 187 | Vector.prototype.lerp = function lerp(x, y, z, amt) { 188 | if (x instanceof Vector) { 189 | return this.lerp(x.x, x.y, x.z, y); 190 | } 191 | this.x += (x - this.x) * amt || 0; 192 | this.y += (y - this.y) * amt || 0; 193 | this.z += (z - this.z) * amt || 0; 194 | return this; 195 | }; 196 | 197 | Vector.prototype.array = function array() { 198 | return [this.x || 0, this.y || 0, this.z || 0]; 199 | }; 200 | 201 | Vector.prototype.equals = function equals(x, y, z) { 202 | var a, b, c; 203 | if (x instanceof Vector) { 204 | a = x.x || 0; 205 | b = x.y || 0; 206 | c = x.z || 0; 207 | } else if (x instanceof Array) { 208 | a = x[0] || 0; 209 | b = x[1] || 0; 210 | c = x[2] || 0; 211 | } else { 212 | a = x || 0; 213 | b = y || 0; 214 | c = z || 0; 215 | } 216 | return this.x === a && this.y === b && this.z === c; 217 | }; 218 | 219 | Vector.fromAngle = function fromAngle(angle, length) { 220 | if (typeof length === 'undefined') { 221 | length = 1; 222 | } 223 | return new Vector(length * Math.cos(angle), length * Math.sin(angle), 0); 224 | }; 225 | 226 | Vector.fromAngles = function(theta, phi, length) { 227 | if (typeof length === 'undefined') { 228 | length = 1; 229 | } 230 | var cosPhi = Math.cos(phi); 231 | var sinPhi = Math.sin(phi); 232 | var cosTheta = Math.cos(theta); 233 | var sinTheta = Math.sin(theta); 234 | 235 | return new Vector( 236 | length * sinTheta * sinPhi, 237 | -length * cosTheta, 238 | length * sinTheta * cosPhi, 239 | ); 240 | }; 241 | 242 | Vector.random2D = function random2D() { 243 | return this.fromAngle(Math.random() * 2 * Math.PI); 244 | }; 245 | 246 | Vector.random3D = function random3D() { 247 | var angle = Math.random() * 2 * Math.PI; 248 | var vz = Math.random() * 2 - 1; 249 | var vzBase = Math.sqrt(1 - vz * vz); 250 | var vx = vzBase * Math.cos(angle); 251 | var vy = vzBase * Math.sin(angle); 252 | return new Vector(vx, vy, vz); 253 | }; 254 | 255 | Vector.add = function add(v1, v2, target) { 256 | if (!target) { 257 | target = v1.copy(); 258 | } else { 259 | target.set(v1); 260 | } 261 | target.add(v2); 262 | return target; 263 | }; 264 | 265 | Vector.sub = function sub(v1, v2, target) { 266 | if (!target) { 267 | target = v1.copy(); 268 | } else { 269 | target.set(v1); 270 | } 271 | target.sub(v2); 272 | return target; 273 | }; 274 | 275 | Vector.mult = function mult(v, n, target) { 276 | if (!target) { 277 | target = v.copy(); 278 | } else { 279 | target.set(v); 280 | } 281 | target.mult(n); 282 | return target; 283 | }; 284 | 285 | Vector.div = function div(v, n, target) { 286 | if (!target) { 287 | target = v.copy(); 288 | } else { 289 | target.set(v); 290 | } 291 | target.div(n); 292 | return target; 293 | }; 294 | 295 | Vector.dot = function dot(v1, v2) { 296 | return v1.dot(v2); 297 | }; 298 | 299 | Vector.cross = function cross(v1, v2) { 300 | return v1.cross(v2); 301 | }; 302 | 303 | Vector.dist = function dist(v1, v2) { 304 | return v1.dist(v2); 305 | }; 306 | 307 | Vector.lerp = function lerp(v1, v2, amt, target) { 308 | if (!target) { 309 | target = v1.copy(); 310 | } else { 311 | target.set(v1); 312 | } 313 | target.lerp(v2, amt); 314 | return target; 315 | }; 316 | 317 | Vector.mag = function mag(vecT) { 318 | var x = vecT.x, 319 | y = vecT.y, 320 | z = vecT.z; 321 | var magSq = x * x + y * y + z * z; 322 | return Math.sqrt(magSq); 323 | }; 324 | 325 | export function createVector(x, y, z) { 326 | return new Vector(x, y, z); 327 | } 328 | 329 | const DEG_TO_RAD = Math.PI / 180; 330 | const RAD_TO_DEG = 180 / Math.PI; 331 | 332 | export function toRadians(angle) { 333 | return angle * DEG_TO_RAD; 334 | } 335 | 336 | export function fromRadians(angle) { 337 | return angle * RAD_TO_DEG; 338 | } 339 | -------------------------------------------------------------------------------- /src/FlagObjectBuilder/Controls.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Flex, Label, Heading, InputRange } from '../primitives'; 3 | import { Toggle } from '../Toggle'; 4 | import { RadioGroup } from '../RadioGroup'; 5 | 6 | const Togglable = ({ name, value, onChange }) => ( 7 | 8 | 11 | onChange(v => !v)} 16 | /> 17 | 18 | ); 19 | 20 | const JustifyContent = ({ label, value, onChange }) => ( 21 | 22 | 30 | 41 | 42 | ); 43 | 44 | export const Controls = ({ 45 | toggles, 46 | alignment, 47 | sliders, 48 | contentAlignment, 49 | contentOrder, 50 | setContentOrder, 51 | ...props 52 | }) => { 53 | return ( 54 | 55 | 56 | Controls 57 | 58 | 66 | Visibility 67 | 68 | {toggles.map(toggle => ( 69 | 70 | ))} 71 | 79 | Alignment 80 | 81 | {alignment.map(radioGrp => ( 82 | 93 | ))} 94 | {contentAlignment.map(section => ( 95 | 96 | ))} 97 | 105 | Size 106 | 107 | {sliders.map(slider => ( 108 | 109 | 110 | 111 | 112 | ))} 113 | 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /src/FlagObjectBuilder/FlagObject.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Flex, Icon, Hidden, Box } from '../primitives'; 3 | import { 4 | Card, 5 | Headline, 6 | StandFirst, 7 | Meta, 8 | MetaLink, 9 | Pillar, 10 | Media, 11 | Blocky, 12 | } from './components'; 13 | 14 | const story = { 15 | headline: 16 | 'Manchester United agree £80m deal to buy Harry Maguire from Leicester', 17 | standFirst: 18 | 'Manchester United have agreed an £80m deal to buy Harry Maguire from Leicester City, making him the world’s most expensive defender', 19 | meta: '2 Aug 2019', 20 | media: 21 | 'https://i.guim.co.uk/img/media/974ef47a5a96b017510dca7371c48fd133d416c1/2_125_3522_2113/master/3522.jpg?width=470&quality=45&auto=format&fit=max&dpr=2&s=0b4979f9faaefbfbd015dd77d22d2d4b', 22 | }; 23 | 24 | export const FlagObject = ({ 25 | headline, 26 | pillar, 27 | standFirst, 28 | meta, 29 | media, 30 | contentHidden, 31 | border, 32 | contentDir, 33 | standfirstDir, 34 | metaDir, 35 | width, 36 | height, 37 | mediaPadding, 38 | textAlignment, 39 | metaAlignment, 40 | reverseContentOrder, 41 | mediaTextRadio, 42 | }) => ( 43 | 9 ? 'auto' : height} 47 | backgroundColor="neutral.6" 48 | justifyContent="stretch" 49 | borderWidth={border ? 2 : 0} 50 | > 51 | 57 | {headline && ( 58 | 59 | {pillar && Football}{' '} 60 | 61 | {story.headline} 62 | 63 | 64 | )} 65 | 66 | {standFirst && ( 67 | 68 | 69 | {story.standFirst} 70 | 71 | 72 | )} 73 | {meta && ( 74 | 80 | )} 81 | 82 | 83 | {media && ( 84 | 85 | 86 | 87 | )} 88 | 89 | ); 90 | 91 | const MetaContent = ({ contentHidden, flexDirection, ...props }) => ( 92 | 98 | 105 | 113 | 114 | {' '} 115 | Published: 2 Aug 2019 116 | 117 | 123 | 132 | 133 | {' '} 134 | 32 135 | 136 | 137 | ); 138 | -------------------------------------------------------------------------------- /src/FlagObjectBuilder/FlagObjectBuilder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThemeProvider } from 'emotion-theming'; 3 | import { Global } from '@emotion/core'; 4 | import theme, { globalStyles } from './theme'; 5 | import { Flex } from '../primitives'; 6 | import { FlagObject } from './FlagObject'; 7 | import { Controls } from './Controls'; 8 | 9 | export function FlagObjectBuilder() { 10 | const controls = useControls(); 11 | 12 | return ( 13 | 14 | 15 | 16 | 25 | 26 | 27 | 36 | 37 | 38 | ); 39 | } 40 | 41 | function useControls() { 42 | const [contentHidden, setContentHidden] = React.useState(true); 43 | const [headline, setHeadline] = React.useState(true); 44 | const [pillar, setPillar] = React.useState(true); 45 | const [standFirst, setStandFirst] = React.useState(true); 46 | const [meta, setMeta] = React.useState(true); 47 | const [media, setMedia] = React.useState(true); 48 | const [border, setBorder] = React.useState(true); 49 | const [reverseContentOrder, setReverseContentOrder] = React.useState(true); 50 | 51 | const [contentDir, setContentDir] = React.useState('column'); 52 | const [standfirstDir, setStandfirstDir] = React.useState('column'); 53 | const [metaDir, setMetaDir] = React.useState('row'); 54 | const [textAlignment, setTextAlignment] = React.useState('flex-start'); 55 | const [metaAlignment, setMetaAlignment] = React.useState('flex-start'); 56 | 57 | const [width, setWidth] = React.useState(10); 58 | const [height, setHeight] = React.useState(10); 59 | const [mediaPadding, setMediaPadding] = React.useState(0); 60 | const [mediaTextRadio, setMediaTextRadio] = React.useState(66); 61 | 62 | return { 63 | toggles: [ 64 | { 65 | name: 'Blocky Content', 66 | value: contentHidden, 67 | onChange: setContentHidden, 68 | }, 69 | { name: 'Headline', value: headline, onChange: setHeadline }, 70 | { name: 'Pillar', value: pillar, onChange: setPillar }, 71 | { name: 'Stand First', value: standFirst, onChange: setStandFirst }, 72 | { name: 'Meta', value: meta, onChange: setMeta }, 73 | { name: 'Media', value: media, onChange: setMedia }, 74 | { name: 'Border', value: border, onChange: setBorder }, 75 | { 76 | name: 'Reverse Content Order', 77 | value: reverseContentOrder, 78 | onChange: setReverseContentOrder, 79 | }, 80 | ], 81 | alignment: [ 82 | { title: 'Content', selected: contentDir, onChange: setContentDir }, 83 | { 84 | title: 'Standfirst Group', 85 | selected: standfirstDir, 86 | onChange: setStandfirstDir, 87 | }, 88 | { 89 | title: 'Meta', 90 | selected: metaDir, 91 | onChange: setMetaDir, 92 | }, 93 | ], 94 | contentAlignment: [ 95 | { 96 | label: 'Text Content', 97 | value: textAlignment, 98 | onChange: setTextAlignment, 99 | }, 100 | { 101 | label: 'Meta Content', 102 | value: metaAlignment, 103 | onChange: setMetaAlignment, 104 | }, 105 | ], 106 | sliders: [ 107 | { name: 'Width', value: width, onChange: setWidth, min: 0, max: 12 }, 108 | // { name: 'Height', value: height, onChange: setHeight, min: 5, max: 10 }, 109 | { 110 | name: 'Media Text Ratio', 111 | value: mediaTextRadio, 112 | onChange: setMediaTextRadio, 113 | min: 0, 114 | max: 100, 115 | }, 116 | { 117 | name: 'Media Padding', 118 | value: mediaPadding, 119 | onChange: setMediaPadding, 120 | min: 0, 121 | max: 8, 122 | }, 123 | ], 124 | flagObjectProps: { 125 | headline, 126 | pillar, 127 | standFirst, 128 | meta, 129 | media, 130 | contentHidden, 131 | reverseContentOrder, 132 | border, 133 | contentDir, 134 | standfirstDir, 135 | metaDir, 136 | width, 137 | height, 138 | mediaPadding, 139 | textAlignment, 140 | metaAlignment, 141 | mediaTextRadio, 142 | }, 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /src/FlagObjectBuilder/components.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { space, color, layout, flexbox, typography } from 'styled-system'; 3 | import { themeGet } from '@styled-system/theme-get'; 4 | import { 5 | Text, 6 | Heading, 7 | AspectRatioImage, 8 | Card as BaseCard, 9 | } from '../primitives'; 10 | 11 | export const Card = styled(BaseCard)(); 12 | Card.defaultProps = { 13 | borderColor: 'pillar.main', 14 | borderWidth: 2, 15 | borderStyle: 'solid', 16 | borderX: 'none', 17 | borderBottom: 'none', 18 | flexDirection: 'column', 19 | }; 20 | 21 | export const Headline = styled(Heading)(); 22 | 23 | Headline.defaultProps = { 24 | as: 'h2', 25 | mb: 3, 26 | mt: 0, 27 | fontFamily: 'serif', 28 | fontWeight: 'bold', 29 | lineHeight: 'title', 30 | fontSize: [2, 3, 4], 31 | color: 'neutral.0', 32 | }; 33 | 34 | export const StandFirst = styled(Text)(); 35 | 36 | StandFirst.defaultProps = { 37 | as: 'p', 38 | mb: 0, 39 | mt: 0, 40 | fontFamily: 'serif', 41 | lineHeight: 'copy', 42 | color: 'neutral.0', 43 | fontSize: 1, 44 | }; 45 | 46 | export const Pillar = styled(Heading)` 47 | ${blocky} 48 | :after { 49 | content: '/'; 50 | display: inline-block; 51 | font-weight: bold; 52 | margin-left: ${props => props.theme.space[1]}px; 53 | } 54 | `; 55 | 56 | Pillar.defaultProps = { 57 | as: 'span', 58 | fontFamily: 'serif', 59 | color: 'pillar.main', 60 | }; 61 | 62 | export const Meta = styled.span( 63 | { display: 'flex' }, 64 | space, 65 | color, 66 | layout, 67 | flexbox, 68 | typography, 69 | blocky, 70 | ); 71 | Meta.defaultProps = { 72 | alignItems: 'center', 73 | fontFamily: 'sans', 74 | lineHeight: 'solid', 75 | fontSize: 0, 76 | color: 'neutral.3', 77 | }; 78 | 79 | export const MetaLink = styled(Meta)( 80 | { 81 | textDecoration: 'none', 82 | display: 'flex', 83 | ':hover, :focus': { 84 | textDecoration: 'underline', 85 | }, 86 | }, 87 | blocky, 88 | ); 89 | 90 | MetaLink.defaultProps = { 91 | as: 'a', 92 | alignItems: 'center', 93 | fontFamily: 'sans', 94 | lineHeight: 'solid', 95 | fontSize: 0, 96 | color: 'neutral.3', 97 | }; 98 | 99 | export const Media = styled(AspectRatioImage)` 100 | position: relative; 101 | 102 | :after { 103 | content: ''; 104 | position: absolute; 105 | background-color: ${props => props.theme.colors.neutral[4]}; 106 | top: 0; 107 | right: 0; 108 | bottom: 0; 109 | left: 0; 110 | z-index: 1; 111 | opacity: ${props => (props.blocky ? 1 : 0)}; 112 | } 113 | `; 114 | 115 | export const Blocky = styled.span(blocky); 116 | 117 | function blocky(props) { 118 | return { 119 | backgroundColor: props.blocky 120 | ? themeGet(`colors.${props.color}`, 'black')(props) 121 | : 'transparent', 122 | ...(props.blocky && { '> *': { opacity: 0 } }), 123 | ...(props.blocky && { color: 'transparent', lineHeight: 1.4 }), 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/FlagObjectBuilder/theme.js: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/core'; 2 | 3 | export default { 4 | breakpoints: ['40em', '52em', '64em', '72em'], 5 | colors: { 6 | pillar: { 7 | dark: '#005689', 8 | main: '#0084c6', 9 | bright: '#00b2ff', 10 | pastel: '#90dcff', 11 | faded: '#f1f8fc', 12 | }, 13 | neutral: [ 14 | '#121212', // 0 brightness 7 15 | '#333333', // 1 brightness 20 16 | '#767676', // 2 brightness 46 17 | '#999999', // 3 brightness 60 18 | '#dcdcdc', // 4 brightness 86 19 | '#ededed', // 5 brightness 93 20 | '#f6f6f6', // 6 brightness 97 21 | '#ffffff', // 7 brightness 100 22 | ], 23 | black: '#121212', 24 | primary: '#78f0a6', 25 | white: '#ffffff', 26 | gray: '#767676', 27 | }, 28 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512], 29 | sizes: [16, 32, 64, 80, 128, 192, 256, 288, 384, 512, 768, 1024, 1536], 30 | radii: [0, 8, 16, 9999, '100%'], 31 | fontSizes: [12, 14, 16, 20, 24, 36, 48, 80, 96], 32 | fontWeights: [100, 200, 300, 400, 500, 600, 700, 800, 900], 33 | fonts: { 34 | serif: '"Libre Baskerville", Georgia, serif', 35 | sans: 36 | '-apple-system, BlinkMacSystemFont, "avenir next", avenir, "helvetica neue", helvetica, ubuntu, roboto, noto, "segoe ui", arial, sans-serif', 37 | }, 38 | lineHeights: { 39 | solid: 1, 40 | title: 1.25, 41 | copy: 1.5, 42 | }, 43 | letterSpacings: { 44 | normal: 'normal', 45 | tracked: '0.1em', 46 | tight: '-0.05em', 47 | mega: '0.25em', 48 | }, 49 | }; 50 | 51 | /* */ 52 | export const globalStyles = css` 53 | /* latin-ext */ 54 | @font-face { 55 | font-family: 'Libre Baskerville'; 56 | font-style: italic; 57 | font-weight: 400; 58 | font-display: swap; 59 | src: local('Libre Baskerville Italic'), local('LibreBaskerville-Italic'), 60 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKhZrc3Hgbbcjq75U4uslyuy4kn0qNcWx8QDO-WyrubOA.woff2) 61 | format('woff2'); 62 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 63 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 64 | } 65 | /* latin */ 66 | @font-face { 67 | font-family: 'Libre Baskerville'; 68 | font-style: italic; 69 | font-weight: 400; 70 | font-display: swap; 71 | src: local('Libre Baskerville Italic'), local('LibreBaskerville-Italic'), 72 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKhZrc3Hgbbcjq75U4uslyuy4kn0qNcWxEQDO-Wyrs.woff2) 73 | format('woff2'); 74 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 75 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 76 | U+2215, U+FEFF, U+FFFD; 77 | } 78 | /* latin-ext */ 79 | @font-face { 80 | font-family: 'Libre Baskerville'; 81 | font-style: normal; 82 | font-weight: 400; 83 | font-display: swap; 84 | src: local('Libre Baskerville'), local('LibreBaskerville-Regular'), 85 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKnZrc3Hgbbcjq75U4uslyuy4kn0qNXaxMaC82U-ro.woff2) 86 | format('woff2'); 87 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 88 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 89 | } 90 | /* latin */ 91 | @font-face { 92 | font-family: 'Libre Baskerville'; 93 | font-style: normal; 94 | font-weight: 400; 95 | font-display: swap; 96 | src: local('Libre Baskerville'), local('LibreBaskerville-Regular'), 97 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKnZrc3Hgbbcjq75U4uslyuy4kn0qNZaxMaC82U.woff2) 98 | format('woff2'); 99 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 100 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 101 | U+2215, U+FEFF, U+FFFD; 102 | } 103 | /* latin-ext */ 104 | @font-face { 105 | font-family: 'Libre Baskerville'; 106 | font-style: normal; 107 | font-weight: 700; 108 | font-display: swap; 109 | src: local('Libre Baskerville Bold'), local('LibreBaskerville-Bold'), 110 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKiZrc3Hgbbcjq75U4uslyuy4kn0qviTgY5KcC-wLOjAUw.woff2) 111 | format('woff2'); 112 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 113 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 114 | } 115 | /* latin */ 116 | @font-face { 117 | font-family: 'Libre Baskerville'; 118 | font-style: normal; 119 | font-weight: 700; 120 | font-display: swap; 121 | src: local('Libre Baskerville Bold'), local('LibreBaskerville-Bold'), 122 | url(https://fonts.gstatic.com/s/librebaskerville/v7/kmKiZrc3Hgbbcjq75U4uslyuy4kn0qviTgY3KcC-wLOj.woff2) 123 | format('woff2'); 124 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 125 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 126 | U+2215, U+FEFF, U+FFFD; 127 | } 128 | 129 | body { 130 | background-color: #fff; 131 | cursor: auto; 132 | } 133 | `; 134 | -------------------------------------------------------------------------------- /src/GenerativeLayout/Article.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { Card } from 'rebass'; 3 | import { Card } from '../primitives'; 4 | 5 | export default [ 6 |
7 |

8 | A Night of Taking Photos at the Spooky Ruins of the Sutro Baths 9 |

10 |
, 11 | , 14 | , 21 |

22 | On , the Sutro Baths were opened to the public as 23 | the world's largest indoor swimming pool establishment. 24 |

, 25 | , 32 |

33 | Before it burned to the ground, the structure filled a small beach inlet 34 | below the Cliff House, also owned by Adolph Sutro at the time. Shortly after 35 | closing, a fire in 1966 destroyed the building while it was in the process 36 | of being demolished. 37 |

, 38 | , 45 |

46 | During high tides, water would flow directly into the pools from the nearby 47 | ocean, recycling the two million US gallons of water in about an hour. 48 |

, 49 | , 56 |

57 | All that remains of the site are concrete walls, blocked off stairs and 58 | passageways, and a tunnel with a deep crevice in the middle. The cause of 59 | the fire was arson. Shortly afterwards, the developer left San Francisco and 60 | claimed insurance money. 61 |

, 62 | , 69 |

70 | During low tides, a powerful turbine water pump, built inside a cave at sea 71 | level, could be switched on from a control room and could fill the tanks at 72 | a rate of 6,000 US gallons a minute, recycling all the water in five hours. 73 |

, 74 | ]; 75 | -------------------------------------------------------------------------------- /src/GenerativeLayout/GenerativeGrid.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { useDebugGrid } from './useDebugGrid'; 3 | import article from './Article'; 4 | import { GeneratorContainer } from './GeneratorContainer'; 5 | 6 | const GridItem = ({ idx, x, y, Component, debug, pinned }) => { 7 | const { togglePin } = useContext(GeneratorContainer.Context); 8 | 9 | return ( 10 |
togglePin(idx)} 19 | > 20 | {article[idx]} 21 |
22 | ); 23 | }; 24 | 25 | export const GenerativeGrid = ({ row, col, size, gap, style }) => { 26 | const gridStyles = { 27 | display: 'grid', 28 | gridTemplateColumns: `repeat(${col}, ${size})`, 29 | gridTemplateRows: `repeat(${row}, ${size})`, 30 | gridGap: gap, 31 | padding: gap, 32 | gridAutoRows: size, 33 | gridAutoColumns: size, 34 | gridAutoFlow: 'dense', 35 | }; 36 | 37 | const DebugGrid = useDebugGrid(4, 6); 38 | const { items, debug } = useContext(GeneratorContainer.Context); 39 | 40 | return ( 41 |
42 |
46 | {items.map(item => ( 47 | 48 | ))} 49 | 50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/GenerativeLayout/GeneratorContainer.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import createContainer from 'constate'; 3 | import { range, randomSize } from './utils'; 4 | 5 | const generateGridItems = count => 6 | range(count).map(idx => ({ 7 | key: idx, 8 | ...randomSize(4, 6), 9 | })); 10 | 11 | const reGenerateGridItems = items => 12 | items.map(item => 13 | item.pinned 14 | ? item 15 | : { 16 | ...item, 17 | ...randomSize(4, 6), 18 | }, 19 | ); 20 | 21 | function useGeneratorState({ initCount = 4 }) { 22 | const [items, setItems] = useState(generateGridItems(initCount)); 23 | const [debug, setDebugGrid] = useState(true); 24 | 25 | return { 26 | items: items, 27 | debug, 28 | togglePin: key => { 29 | const gridItems = items.map(item => 30 | item.key === key ? { ...item, pinned: !item.pinned } : item, 31 | ); 32 | setItems(gridItems); 33 | }, 34 | toggleDebugGrid: () => { 35 | setDebugGrid(!debug); 36 | }, 37 | onChange: e => { 38 | const count = Number(e.target.value); 39 | const gridItems = generateGridItems(count); 40 | setItems(gridItems); 41 | }, 42 | randomGrid: () => { 43 | const gridItems = reGenerateGridItems(items); 44 | setItems(gridItems); 45 | }, 46 | }; 47 | } 48 | 49 | export const GeneratorContainer = createContainer(useGeneratorState); 50 | -------------------------------------------------------------------------------- /src/GenerativeLayout/GeneratorControls.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { GeneratorContainer } from './GeneratorContainer'; 3 | 4 | export const GeneratorControls = () => { 5 | const { items, onChange, randomGrid, toggleDebugGrid } = useContext( 6 | GeneratorContainer.Context, 7 | ); 8 | 9 | return ( 10 |
11 |
12 |
13 | 16 | 27 |
28 | 29 | 37 | 45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/GenerativeLayout/generative-layout.css: -------------------------------------------------------------------------------- 1 | .input-range { 2 | vertical-align: middle; 3 | background-color: transparent; 4 | padding-top: 8px; 5 | padding-top: 0.5rem; 6 | padding-bottom: 8px; 7 | padding-bottom: 0.5rem; 8 | color: inherit; 9 | background-color: transparent; 10 | -webkit-appearance: none; 11 | } 12 | 13 | .input-range::-webkit-slider-thumb { 14 | position: relative; 15 | width: 8px; 16 | width: 0.5rem; 17 | height: 20px; 18 | height: 1.25rem; 19 | cursor: pointer; 20 | margin-top: -8px; 21 | margin-top: -0.5rem; 22 | border-radius: 3px; 23 | background-color: currentcolor; 24 | -webkit-appearance: none; 25 | } 26 | 27 | /* Touch screen friendly pseudo element */ 28 | .input-range::-webkit-slider-thumb:before { 29 | content: ''; 30 | display: block; 31 | position: absolute; 32 | top: -8px; 33 | top: -0.5rem; 34 | left: -14px; 35 | left: -0.875rem; 36 | width: 36px; 37 | width: 2.25rem; 38 | height: 36px; 39 | height: 2.25rem; 40 | opacity: 0; 41 | } 42 | 43 | .input-range::-moz-range-thumb { 44 | width: 8px; 45 | width: 0.5rem; 46 | height: 20px; 47 | height: 1.25rem; 48 | cursor: pointer; 49 | border-radius: 3px; 50 | border-color: transparent; 51 | border-width: 0; 52 | background-color: currentcolor; 53 | } 54 | 55 | .input-range::-webkit-slider-runnable-track { 56 | height: 4px; 57 | height: 0.25rem; 58 | cursor: pointer; 59 | border-radius: 3px; 60 | background-color: rgba(0, 0, 0, 0.25); 61 | } 62 | 63 | .input-range::-moz-range-track { 64 | height: 4px; 65 | height: 0.25rem; 66 | cursor: pointer; 67 | border-radius: 3px; 68 | background-color: rgba(0, 0, 0, 0.25); 69 | } 70 | 71 | .input-range:focus { 72 | outline: none; 73 | } 74 | -------------------------------------------------------------------------------- /src/GenerativeLayout/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'tachyons'; 3 | import './generative-layout.css'; 4 | import { Global } from '@emotion/core'; 5 | 6 | import { GenerativeGrid } from './GenerativeGrid'; 7 | import { GeneratorControls } from './GeneratorControls'; 8 | import { GeneratorContainer } from './GeneratorContainer'; 9 | 10 | export const GenerativeLayout = () => { 11 | return ( 12 | 13 | 21 | 22 | 23 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/GenerativeLayout/useDebugGrid.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { range } from './utils'; 3 | 4 | export function useDebugGrid(initialRowCount, initialColumnCount) { 5 | const gridEl = useRef(null); 6 | 7 | const [rowCount, setRowCount] = useState(initialRowCount); 8 | const [columnCount, setColumnCount] = useState(initialColumnCount); 9 | 10 | useEffect(() => { 11 | const size = window.innerWidth * 0.15; 12 | const height = gridEl.current.getBoundingClientRect().height; 13 | const width = gridEl.current.getBoundingClientRect().width; 14 | setRowCount(Math.floor(height / size)); 15 | setColumnCount(Math.floor(width / size)); 16 | }, []); 17 | 18 | const itemCount = rowCount * columnCount; 19 | 20 | const DebugGrid = ({ definition, visible }) => ( 21 |
26 | {range(itemCount).map(idx => ( 27 |
28 | ))} 29 |
30 | ); 31 | 32 | return DebugGrid; 33 | } 34 | -------------------------------------------------------------------------------- /src/GenerativeLayout/utils.js: -------------------------------------------------------------------------------- 1 | export const range = n => Array.from(Array(n).keys()); 2 | 3 | export const randomRange = (min, max) => { 4 | if (max === undefined) { 5 | max = min; 6 | min = 0; 7 | } 8 | 9 | const value = Math.random() * (max - min) + min; 10 | return Math.floor(value); 11 | }; 12 | 13 | export const randomSize = (w, h) => ({ 14 | x: randomRange(1, w), 15 | y: randomRange(1, h), 16 | pinned: false, 17 | align: 'center', 18 | }); 19 | 20 | export const pickRandom = items => 21 | items[Math.floor(Math.random() * items.length)]; 22 | -------------------------------------------------------------------------------- /src/GenerativePalette/DynamicCanvas.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTheme } from 'emotion-theming'; 3 | import { 4 | TypographySwatch, 5 | ColorSwatch, 6 | NavBar, 7 | MediaCard, 8 | SearchBar, 9 | ProfileCard, 10 | } from '../components'; 11 | import { ComponentGrid } from '../ComponentGrid'; 12 | import { Pager } from '../Pager'; 13 | 14 | function DynamicCanvas({ 15 | profile, 16 | media, 17 | activeIndex, 18 | selectTheme, 19 | children, 20 | theme, 21 | ...props 22 | }) { 23 | console.log(props); 24 | 25 | return ( 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export default withTheme(DynamicCanvas); 62 | -------------------------------------------------------------------------------- /src/GenerativePalette/GenerativePalette.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ThemeProvider } from 'emotion-theming'; 3 | import theme from '../theme'; 4 | import { Container } from '../primitives'; 5 | import { useThemes } from '../useThemes'; 6 | import DynamicCanvas from './DynamicCanvas'; 7 | import { Sliders } from './Sliders'; 8 | import { useGenerativePalette } from './useGenerativePalette'; 9 | 10 | export function GenerativePalette() { 11 | const [data, activeIndex, selectTheme] = useThemes(); 12 | const [ 13 | palette, 14 | hue, 15 | saturation, 16 | adjustHue, 17 | adjustSaturation, 18 | ] = useGenerativePalette(); 19 | const [roundness, radii, adjustRoundness] = useRadii(); 20 | const [spaciousness, space, adjustSpaciousness] = useSpace(); 21 | 22 | return ( 23 | 24 | 25 | 42 | 50 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | function useRadii() { 65 | const [roundness, setRoundness] = useState(1); 66 | const [radii, setRadii] = useState([0, 8, 16, 9999, '100%']); 67 | const adjustRoundness = roundness => { 68 | setRoundness(roundness); 69 | setRadii([...[0, 8, 16].map(x => x * roundness), 9999, '100%']); 70 | }; 71 | 72 | return [roundness, radii, adjustRoundness]; 73 | } 74 | 75 | function useSpace() { 76 | const [spaciousness, setSpaciousness] = useState(1); 77 | const [space, setSpace] = useState([0, 4, 8, 16, 32, 64, 128, 256, 512]); 78 | const adjustSpaciousness = spaciousness => { 79 | setSpaciousness(spaciousness); 80 | setSpace([0, 4, 8, 16, 32, 64, 128, 256, 512].map(x => x * spaciousness)); 81 | }; 82 | 83 | return [spaciousness, space, adjustSpaciousness]; 84 | } 85 | -------------------------------------------------------------------------------- /src/GenerativePalette/Sliders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InputRange, Label, Card, Box } from '../primitives'; 3 | 4 | export const Sliders = ({ 5 | hue, 6 | adjustHue, 7 | saturation, 8 | adjustSaturation, 9 | roundness, 10 | adjustRoundness, 11 | spaciousness, 12 | adjustSpaciousness, 13 | ...props 14 | }) => ( 15 | 24 | 25 | 26 | 35 | 36 | 37 | 38 | 39 | 48 | 49 | 50 | 51 | 52 | 61 | 62 | 63 | 64 | 65 | 74 | 75 | 76 | ); 77 | -------------------------------------------------------------------------------- /src/GenerativePalette/useGenerativePalette.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import hsluv from 'hsluv'; 3 | import Random from 'canvas-sketch-util/random'; 4 | import { lerp } from 'canvas-sketch-util/math'; 5 | 6 | function colourPalette(hue, saturation) { 7 | const black = hsluv.hsluvToHex([ 8 | 0, 9 | Random.rangeFloor(0, 25), 10 | Random.rangeFloor(0, 10), 11 | ]); 12 | 13 | const gray = hsluv.hsluvToHex([0, 0, Random.rangeFloor(40, 60)]); 14 | 15 | const h = i => lerp(hue - 40, hue, i); 16 | const s = i => lerp(saturation - 4, saturation, i); 17 | const l = i => lerp(96, 56, i); 18 | 19 | const tertiary = hsluv.hsluvToHex([h(1 / 2), s(1 / 2), l(1 / 2)]); 20 | const secondary = hsluv.hsluvToHex([h(0 / 2), s(0 / 2), l(0 / 2)]); 21 | const primary = hsluv.hsluvToHex([h(2 / 2), s(2 / 2), l(2 / 2)]); 22 | return { 23 | black, 24 | gray, 25 | primary, 26 | tertiary, 27 | secondary, 28 | }; 29 | } 30 | 31 | export function useGenerativePalette() { 32 | const start = { h: Random.rangeFloor(0, 360), s: Random.rangeFloor(0, 100) }; 33 | const [palette, setPalette] = useState(colourPalette(start.h, start.s)); 34 | const [hue, setHue] = useState(start.h); 35 | const [saturation, setSaturation] = useState(start.s); 36 | 37 | const adjustHue = value => { 38 | setHue(value); 39 | setPalette(colourPalette(hue, saturation)); 40 | }; 41 | 42 | const adjustSaturation = value => { 43 | setSaturation(value); 44 | setPalette(colourPalette(hue, saturation)); 45 | }; 46 | 47 | return [palette, hue, saturation, adjustHue, adjustSaturation]; 48 | } 49 | -------------------------------------------------------------------------------- /src/Info.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Dialog } from '@reach/dialog'; 4 | import '@reach/dialog/styles.css'; 5 | import { 6 | IconButton, 7 | Info, 8 | Close, 9 | Heading, 10 | Flex, 11 | Text, 12 | Link, 13 | } from './primitives'; 14 | 15 | const ShowInfoButton = styled(IconButton)` 16 | position: absolute; 17 | right: 0; 18 | top: 0; 19 | padding: ${props => props.theme.space[3]}px; 20 | `; 21 | 22 | const StyledDialog = styled(Dialog)` 23 | box-sizing: border-box; 24 | padding: ${props => props.theme.space[4]}px; 25 | border-radius: ${props => props.theme.radii[2]}px; 26 | max-width: 40em; 27 | width: 100%; 28 | `; 29 | 30 | export const InfoButton = () => { 31 | const [showInfo, setVisibility] = useState(false); 32 | return ( 33 | 34 | } 38 | onClick={() => setVisibility(true)} 39 | /> 40 | 41 | setVisibility(false)}> 42 | 43 | Generative Lighting 44 | } 48 | onClick={() => setVisibility(false)} 49 | /> 50 | 51 | 52 | 53 | This experiment uses a basic ray casting engine to dynamically 54 | determine the colours of interface. The engine's light source emits 55 | vector rays. The interface provides surfaces for the 56 | light to interact with. The number of rays hitting a particular UI 57 | component determines how much light it is receiving and therefore its 58 | colour. 59 | 60 | 61 | 62 | For more on ray casting check out Daniel Shiffman's fantastic 63 | tutorial:{' '} 64 | 65 | Coding Challenge #145: 2D Raycasting 66 | 67 | . 68 | 69 | 70 | 71 | The design for all the UI components are based on{' '} 72 | usedetails.com. 73 | 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/Pager.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Box, Flex, IconButton } from './primitives'; 4 | import { position } from 'styled-system'; 5 | 6 | const Circle = ({ isActive, ...props }) => ( 7 | 18 | 19 | 20 | ); 21 | 22 | const PagerItem = styled.li` 23 | list-style: none; 24 | display: inline-block; 25 | line-height: 1; 26 | `; 27 | 28 | const PagerNav = styled(Box)({}, position); 29 | 30 | PagerNav.defaultProps = { 31 | position: ['relative', 'absolute'], 32 | bottom: [0, '24px', '32px'], 33 | mt: [3, 0], 34 | mb: [3, 0], 35 | }; 36 | 37 | export const Pager = ({ active, onUpdate, color = '#fff', ...props }) => ( 38 | 39 | 40 | 41 | } 45 | onClick={() => onUpdate(0)} 46 | /> 47 | 48 | 49 | } 53 | onClick={() => onUpdate(1)} 54 | /> 55 | 56 | 57 | } 61 | onClick={() => onUpdate(2)} 62 | /> 63 | 64 | 65 | 66 | ); 67 | -------------------------------------------------------------------------------- /src/RadioGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { Box, Flex, Text, Label } from './primitives'; 4 | 5 | const RadioGroupFieldset = styled(Flex)({ 6 | padding: 0, 7 | border: 0, 8 | }); 9 | RadioGroupFieldset.defaultProps = { 10 | // as: 'fieldset', doesn't support flex 11 | role: 'group', 12 | flexDirection: 'column', 13 | mt: 0, 14 | mb: 0, 15 | mr: 0, 16 | ml: 0, 17 | }; 18 | 19 | const RadioGroupContainer = styled(Flex)({ 20 | marginLeft: 0, 21 | ':hover, :focus-within': { 22 | borderColor: '#e5e5e5', 23 | }, 24 | }); 25 | RadioGroupContainer.defaultProps = { 26 | borderWidth: 2, 27 | borderStyle: 'solid', 28 | borderColor: 'white', 29 | borderRadius: '3px', 30 | }; 31 | 32 | const RadioGroupTitle = styled.legend` 33 | position: fixed !important; 34 | _position: absolute !important; 35 | clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ 36 | clip: rect(1px, 1px, 1px, 1px); 37 | `; 38 | 39 | const RadioInput = styled.input` 40 | opacity: 0; 41 | position: absolute; 42 | 43 | &:focus + label { 44 | border-radius: 0; 45 | } 46 | 47 | &:checked + label { 48 | background-color: #e5e5e5; 49 | } 50 | `; 51 | 52 | const RadioGroupLabel = styled(Text)({ 53 | display: 'flex', 54 | alignItems: 'center', 55 | justifyContent: 'center', 56 | borderRadius: '3px', 57 | }); 58 | 59 | RadioGroupLabel.defaultProps = { 60 | as: 'label', 61 | backgroundColor: '#fff', 62 | lineHeight: 'solid', 63 | color: 'black', 64 | fontWeight: 5, 65 | fontSize: 2, 66 | }; 67 | 68 | export const RadioGroup = ({ 69 | onChange, 70 | selected, 71 | title, 72 | hideTitle = false, 73 | items, 74 | direction = 'column', 75 | size = 2, 76 | ...props 77 | }) => ( 78 | 79 | {hideTitle ? ( 80 | {title} 81 | ) : ( 82 | 85 | )} 86 | 91 | {items.map(item => ( 92 | 93 | onChange(item.value)} 98 | checked={selected === item.value} 99 | /> 100 | 107 | {item.label} 108 | 109 | 110 | ))} 111 | 112 | 113 | ); 114 | -------------------------------------------------------------------------------- /src/Responsive/Artboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TypographySwatch, MediaCard, ProfileCard } from './components'; 3 | 4 | import { Box } from '../primitives'; 5 | import { Dimensions } from './RedLines'; 6 | 7 | export function Artboard({ 8 | profile, 9 | media, 10 | activeIndex, 11 | selectTheme, 12 | children, 13 | theme, 14 | debug, 15 | ...props 16 | }) { 17 | return ( 18 | 19 | ( 22 |
23 | 24 |
25 | )} 26 | /> 27 | 28 | ( 31 |
32 | 33 |
34 | )} 35 | /> 36 | 37 | ( 40 |
41 | 42 |
43 | )} 44 | /> 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/Responsive/RedLines.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import useComponentSize from '@rehooks/component-size'; 3 | import styled from '@emotion/styled'; 4 | import { system, border, typography } from 'styled-system'; 5 | import { Resizable } from 're-resizable'; 6 | import { Box, Text, Flex } from '../primitives'; 7 | 8 | const spacer = system({ 9 | vertical: { 10 | property: 'height', 11 | scale: 'space', 12 | }, 13 | horizontal: { 14 | property: 'width', 15 | scale: 'space', 16 | }, 17 | }); 18 | 19 | export const SpacingY = styled(Flex)( 20 | props => ({ 21 | flex: 'none', 22 | backgroundColor: 23 | props.type === 'margin' 24 | ? 'rgba(245, 88, 88, .16)' 25 | : 'rgba(0, 235, 172, .24)', 26 | color: props.type === 'margin' ? '#B80000' : '#008F5F', 27 | textAlign: 'center', 28 | opacity: !props.visible || props.vertical === 0 ? 0 : 0.75, 29 | display: props.vertical === 0 ? 'none' : 'flex', 30 | }), 31 | spacer, 32 | typography, 33 | ); 34 | 35 | SpacingY.defaultProps = { 36 | type: 'margin', 37 | alignItems: 'center', 38 | justifyContent: 'center', 39 | width: 3, 40 | vertical: 4, 41 | ml: 'auto', 42 | mr: 'auto', 43 | fontSize: '0', 44 | fontWeight: 4, 45 | }; 46 | 47 | export const SpacingX = styled(Flex)( 48 | props => ({ 49 | flex: 'none', 50 | backgroundColor: 51 | props.type === 'margin' 52 | ? 'rgba(245, 88, 88, .16)' 53 | : 'rgba(0, 235, 172, .24)', 54 | color: props.type === 'margin' ? '#B80000' : '#008F5F', 55 | textAlign: 'center', 56 | opacity: !props.visible || props.horizontal === 0 ? 0 : 0.75, 57 | display: props.horizontal === 0 ? 'none' : 'flex', 58 | }), 59 | spacer, 60 | typography, 61 | ); 62 | 63 | SpacingX.defaultProps = { 64 | type: 'margin', 65 | alignItems: 'center', 66 | justifyContent: 'center', 67 | height: 3, 68 | horizontal: 4, 69 | mt: 'auto', 70 | mb: 'auto', 71 | fontSize: '0', 72 | fontWeight: 4, 73 | }; 74 | 75 | const DimensionContainer = styled(Flex)(border); 76 | const DimensionValue = styled(Text)(border); 77 | 78 | export const Dimensions = ({ render, ...props }) => { 79 | const ref = useRef(null); 80 | const size = useComponentSize(ref); 81 | 82 | return ( 83 | 84 | 106 | 113 | 114 | 124 | {size.width} 125 | 126 | 127 | 128 | {render(ref)} 129 | 130 | 131 | ); 132 | }; 133 | -------------------------------------------------------------------------------- /src/Responsive/Responsive.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ThemeProvider } from 'emotion-theming'; 3 | import { Global } from '@emotion/core'; 4 | import theme from '../theme'; 5 | import { useThemes } from '../useThemes'; 6 | import { Artboard } from './Artboard'; 7 | import { Flex } from '../primitives'; 8 | import { RadioGroup } from '../RadioGroup'; 9 | import { Toggle } from '../Toggle'; 10 | 11 | export function Responsive() { 12 | const [data, activeIndex, selectTheme] = useThemes(); 13 | const [debug, setDebug] = useState(false); 14 | console.log(data.theme, activeIndex); 15 | 16 | return ( 17 | 18 | 26 | 27 | 28 | 39 | setDebug(!debug)} 43 | /> 44 | 45 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/Responsive/components.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import useComponentSize from '@rehooks/component-size'; 3 | import theme from '../theme'; 4 | import { 5 | Card, 6 | Text, 7 | Box, 8 | IconButton, 9 | Heading, 10 | BackgroundImage, 11 | Flex, 12 | Input, 13 | Image, 14 | PrimaryButton, 15 | } from '../primitives'; 16 | import { ReactComponent as Menu } from '../icons/menu.svg'; 17 | import { ReactComponent as Search } from '../icons/search.svg'; 18 | import { SpacingY, SpacingX } from './RedLines'; 19 | 20 | function useResponsiveSystem(bps = [640, 832, 1024]) { 21 | const ref = useRef(null); 22 | const { width } = useComponentSize(ref); 23 | 24 | bps.push(width); 25 | bps.sort((a, b) => a - b); 26 | const cq = bps.indexOf(width); 27 | 28 | const rsx = styles => 29 | styles[cq] !== undefined ? styles[cq] : styles[styles.length - 1]; 30 | 31 | return [ref, rsx]; 32 | } 33 | 34 | export const TypographySwatch = props => { 35 | const [ref, rsx] = useResponsiveSystem(); 36 | 37 | return ( 38 | 46 | 47 | Aa 48 | 49 | 50 | ); 51 | }; 52 | 53 | export const MediaCard = ({ image, title, body, debug, ...props }) => { 54 | const [ref, rsx] = useResponsiveSystem([480, 640, 1024]); 55 | 56 | return ( 57 | 64 | 65 | 69 | 70 | {theme.space[rsx([3, 3, 4])]} 71 | 72 | 79 | 80 | {theme.space[rsx([0, 4, 0])]} 81 | 82 | 83 | {title} 84 | 85 | 86 | {theme.space[2]} 87 | 88 | 89 | {body} 90 | 91 | 92 | {theme.space[rsx([0, 4, 0])]} 93 | 94 | 95 | 96 | {theme.space[rsx([3, 3, 4])]} 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | export const ProfileCard = ({ avatar, name, title, debug, ...props }) => { 104 | const [ref, rsx] = useResponsiveSystem([480, 640, 1024]); 105 | 106 | return ( 107 | 116 | 123 | 124 | {theme.space[rsx([0, 4])]} 125 | 126 | 127 | {theme.space[rsx([3, 0])]} 128 | 129 | 130 | 131 | {theme.space[rsx([3, 0])]} 132 | 133 | 138 | 144 | {name} 145 | 146 | 152 | {theme.space[rsx([1, 2])]} 153 | 154 | 160 | {title} 161 | 162 | 168 | {theme.space[rsx([4, 3])]} 169 | 170 | 176 | Follow 177 | 178 | 179 | 180 | {theme.space[rsx([3, 0])]} 181 | 182 | 183 | 184 | ); 185 | }; 186 | 187 | export const ColorSwatch = ({ name, ...props }) => ( 188 | 189 | 190 | 198 | {name} 199 | 200 | 201 | ); 202 | 203 | export const NavBar = ({ name, ...props }) => ( 204 | 205 | } /> 206 | 212 | Details 213 | 214 | } /> 215 | 216 | ); 217 | 218 | export const SearchBar = ({ name, ...props }) => ( 219 | 228 | 231 | ); 232 | -------------------------------------------------------------------------------- /src/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { color, space } from 'styled-system'; 4 | import posed from 'react-pose'; 5 | 6 | const ToggleIndicator = posed.circle({ 7 | off: { 8 | cx: 8, 9 | }, 10 | on: { 11 | cx: 16, 12 | }, 13 | }); 14 | 15 | const ToggleButton = styled.svg` 16 | ${space} 17 | ${color} 18 | `; 19 | 20 | ToggleButton.defaultProps = { 21 | color: 'white', 22 | p: 3, 23 | }; 24 | 25 | export const Toggle = ({ enabled, onClick, ...props }) => ( 26 | { 38 | if (e.key === ' ') { 39 | onClick(); 40 | } 41 | }} 42 | role="button" 43 | tabIndex="0" 44 | aria-pressed={enabled} 45 | {...props} 46 | > 47 | 56 | ); 57 | -------------------------------------------------------------------------------- /src/components.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Card, 4 | Text, 5 | Box, 6 | IconButton, 7 | Heading, 8 | BackgroundImage, 9 | Flex, 10 | Input, 11 | Image, 12 | PrimaryButton, 13 | } from './primitives'; 14 | import { ReactComponent as Menu } from './icons/menu.svg'; 15 | import { ReactComponent as Search } from './icons/search.svg'; 16 | 17 | export const TypographySwatch = React.forwardRef((props, ref) => ( 18 | 26 | 27 | Aa 28 | 29 | 30 | )); 31 | 32 | export const ColorSwatch = React.forwardRef(({ name, ...props }, ref) => ( 33 | 34 | 35 | 43 | {name} 44 | 45 | 46 | )); 47 | 48 | export const NavBar = React.forwardRef(({ name, ...props }, ref) => ( 49 | 58 | } /> 59 | 65 | Details 66 | 67 | } /> 68 | 69 | )); 70 | 71 | export const MediaCard = React.forwardRef( 72 | ({ image, title, body, ...props }, ref) => ( 73 | 74 | 75 | 76 | 77 | {title} 78 | 79 | 80 | {body} 81 | 82 | 83 | 84 | ), 85 | ); 86 | 87 | export const SearchBar = React.forwardRef(({ name, ...props }, ref) => ( 88 | 98 | 101 | )); 102 | 103 | export const ProfileCard = React.forwardRef( 104 | ({ avatar, name, title, ...props }, ref) => ( 105 | 115 | 122 | 129 | 135 | {name} 136 | 137 | 143 | {title} 144 | 145 | 146 | Follow 147 | 148 | 149 | 150 | ), 151 | ); 152 | -------------------------------------------------------------------------------- /src/icons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/layout.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/maximize-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/sliders.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/x-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/imgs/ayla.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/ayla.jpg -------------------------------------------------------------------------------- /src/imgs/ernest-porzi-19106-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/ernest-porzi-19106-unsplash.jpg -------------------------------------------------------------------------------- /src/imgs/fabian-moller-401625-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/fabian-moller-401625-unsplash.jpg -------------------------------------------------------------------------------- /src/imgs/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/light.png -------------------------------------------------------------------------------- /src/imgs/light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/imgs/paridhi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/paridhi.png -------------------------------------------------------------------------------- /src/imgs/sandra-ollier-663002-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/sandra-ollier-663002-unsplash.jpg -------------------------------------------------------------------------------- /src/imgs/zainab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winkerVSbecks/generative-parts/a596f912c66c5f4b0cd02bddab0622a7e18df124/src/imgs/zainab.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | loadPolyfills().then(() => { 8 | ReactDOM.render(, document.getElementById('root')); 9 | }); 10 | 11 | /** 12 | * Do feature detection, to figure out which polyfills needs to be imported. 13 | **/ 14 | function loadPolyfills() { 15 | const polyfills = []; 16 | 17 | if (!supportsResizeObserver()) { 18 | polyfills.push(import('resize-observer-polyfill')); 19 | } 20 | 21 | return Promise.all(polyfills); 22 | } 23 | 24 | function supportsResizeObserver() { 25 | return ( 26 | 'ResizeObserver' in global && 27 | 'ResizeObserverEntry' in global && 28 | 'contentRect' in ResizeObserverEntry.prototype 29 | ); 30 | } 31 | 32 | // If you want your app to work offline and load faster, you can change 33 | // unregister() to register() below. Note this comes with some pitfalls. 34 | // Learn more about service workers: https://bit.ly/CRA-PWA 35 | serviceWorker.unregister(); 36 | -------------------------------------------------------------------------------- /src/primitives.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { 4 | space, 5 | color, 6 | layout, 7 | flexbox, 8 | typography, 9 | border, 10 | shadow, 11 | background, 12 | buttonStyle, 13 | grid, 14 | } from 'styled-system'; 15 | import { withTheme } from 'emotion-theming'; 16 | import 'rc-slider/assets/index.css'; 17 | import Slider from 'rc-slider'; 18 | import { ReactComponent as MenuIcon } from './icons/menu.svg'; 19 | import { ReactComponent as SearchIcon } from './icons/search.svg'; 20 | import { ReactComponent as InfoIcon } from './icons/info.svg'; 21 | import { ReactComponent as CloseIcon } from './icons/x-circle.svg'; 22 | 23 | const StyledSlider = styled(Slider)` 24 | .rc-slider-handle:active { 25 | border: 0; 26 | box-shadow: none; 27 | } 28 | 29 | .rc-slider-handle:focus { 30 | border: 0; 31 | box-shadow: none; 32 | } 33 | `; 34 | 35 | export const InputRange = withTheme(({ theme, ...props }) => ( 36 | 47 | )); 48 | 49 | export const Box = styled.div( 50 | { 51 | boxSizing: 'border-box', 52 | minWidth: 0, 53 | }, 54 | space, 55 | color, 56 | layout, 57 | flexbox, 58 | border, 59 | ); 60 | 61 | export const Flex = styled(Box)({ 62 | display: 'flex', 63 | }); 64 | 65 | export const Text = styled(Box)(typography); 66 | 67 | Text.defaultProps = { 68 | as: 'p', 69 | mb: 0, 70 | mt: 0, 71 | fontFamily: 'inter', 72 | lineHeight: 'copy', 73 | color: 'gray', 74 | }; 75 | 76 | export const Heading = styled(Text)(); 77 | 78 | Heading.defaultProps = { 79 | as: 'h2', 80 | mb: 0, 81 | mt: 0, 82 | fontSize: 4, 83 | fontWeight: 'bold', 84 | fontFamily: 'inter', 85 | lineHeight: 'title', 86 | color: 'black', 87 | }; 88 | 89 | export const Label = styled(Text)({ 90 | display: 'block', 91 | }); 92 | 93 | Label.defaultProps = { 94 | as: 'label', 95 | mb: 2, 96 | mt: 0, 97 | lineHeight: 'solid', 98 | color: 'currentColor', 99 | fontWeight: 5, 100 | fontSize: 1, 101 | }; 102 | 103 | export const Image = styled(Box)( 104 | { 105 | maxWidth: '100%', 106 | }, 107 | border, 108 | ); 109 | 110 | Image.defaultProps = { 111 | as: 'img', 112 | m: 0, 113 | height: 'auto', 114 | }; 115 | 116 | export const BackgroundImage = styled.div( 117 | { 118 | backgroundPosition: 'center', 119 | backgroundSize: 'cover', 120 | backgroundRepeat: 'no-repeat', 121 | }, 122 | props => ({ 123 | backgroundColor: props.theme.colors.gray, 124 | backgroundImage: props.image ? `url(${props.image})` : null, 125 | }), 126 | layout, 127 | flexbox, 128 | ); 129 | 130 | export const AspectRatioImage = styled.div( 131 | { 132 | backgroundPosition: 'center', 133 | backgroundSize: 'cover', 134 | backgroundRepeat: 'no-repeat', 135 | height: 0, 136 | }, 137 | props => ({ 138 | backgroundColor: props.theme.colors.gray, 139 | backgroundImage: props.image ? `url(${props.image})` : null, 140 | paddingBottom: `${(1 / props.aspectRatio) * 100}%`, 141 | }), 142 | space, 143 | layout, 144 | flexbox, 145 | ); 146 | AspectRatioImage.defaultProps = { 147 | aspectRatio: 1, 148 | }; 149 | 150 | export const Card = styled(Box)( 151 | { display: 'flex', overflow: 'hidden' }, 152 | border, 153 | shadow, 154 | background, 155 | grid, 156 | ); 157 | 158 | Card.defaultProps = { 159 | borderRadius: 1, 160 | }; 161 | 162 | export const Button = styled(Box)( 163 | { 164 | appearance: 'none', 165 | display: 'inline-block', 166 | textAlign: 'center', 167 | lineHeight: 'inherit', 168 | textDecoration: 'none', 169 | }, 170 | typography, 171 | border, 172 | buttonStyle, 173 | ); 174 | 175 | Button.defaultProps = { 176 | as: 'button', 177 | border: 0, 178 | }; 179 | 180 | export const PrimaryButton = styled(Button)({ 181 | display: 'block', 182 | backfaceVisibility: 'hidden', 183 | transform: 'translateZ(0)', 184 | transition: 'transform .25s ease-out', 185 | outline: 0, 186 | ':hover,:focus': { 187 | transform: 'scale( 1.05 )', 188 | }, 189 | ':active': { 190 | transform: 'scale( .90 )', 191 | }, 192 | }); 193 | 194 | PrimaryButton.defaultProps = { 195 | as: 'button', 196 | border: 0, 197 | backgroundColor: 'secondary', 198 | borderRadius: 1, 199 | color: 'primary', 200 | fontSize: 1, 201 | fontWeight: 6, 202 | }; 203 | 204 | export const Link = styled(Text)(props => ({ 205 | textDecoration: 'none', 206 | 207 | ':hover, :focus': { 208 | color: props.theme.colors.primary, 209 | }, 210 | })); 211 | 212 | Link.defaultProps = { 213 | as: 'a', 214 | color: 'white', 215 | }; 216 | 217 | export const Container = styled(Flex)({ overflow: 'hidden' }); 218 | 219 | Container.defaultProps = { 220 | alignItems: 'center', 221 | mx: 'auto', 222 | height: '100vh', 223 | maxWidth: 1184, 224 | position: 'relative', 225 | }; 226 | 227 | export const TransparentButton = styled(Button)` 228 | background-color: transparent; 229 | &:hover { 230 | background-color: transparent; 231 | } 232 | & > div { 233 | display: flex; 234 | } 235 | `; 236 | 237 | export const Icon = styled.svg(space, color, layout, flexbox); 238 | Icon.defaultProps = { width: 1, height: 1 }; 239 | export const Menu = styled(MenuIcon)({}, color, space); 240 | export const Search = styled(SearchIcon)({}, color, space); 241 | export const Info = styled(InfoIcon)({}, color, space); 242 | export const Close = styled(CloseIcon)({}, color, space); 243 | 244 | export const IconButton = ({ name, icon, ...props }) => ( 245 | 246 |
{icon}
247 |
248 | ); 249 | 250 | export const Input = styled.input( 251 | { 252 | appearance: 'none', 253 | display: 'block', 254 | width: '100%', 255 | fontFamily: 'inherit', 256 | color: 'inherit', 257 | backgroundColor: 'transparent', 258 | borderWidth: 0, 259 | borderStyle: 'solid', 260 | '::-ms-clear': { 261 | display: 'none', 262 | }, 263 | }, 264 | border, 265 | space, 266 | props => ({ 267 | '::placeholder': { 268 | color: props.theme.gray, 269 | }, 270 | }), 271 | ); 272 | 273 | Input.defaultProps = { 274 | fontSize: 1, 275 | fontWeight: 400, 276 | pt: 3, 277 | pb: 3, 278 | pl: 2, 279 | pr: 2, 280 | borderRadius: 0, 281 | mt: 0, 282 | mb: 0, 283 | ml: 0, 284 | mr: 0, 285 | }; 286 | 287 | export const Hidden = styled.span` 288 | border: 0 !important; 289 | clip: rect(0 0 0 0) !important; 290 | height: 0.0625rem !important; 291 | margin: -0.0625rem !important; 292 | overflow: hidden !important; 293 | padding: 0 !important; 294 | position: absolute !important; 295 | width: 0.0625rem !important; 296 | `; 297 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breakpoints: ['40em', '52em', '64em', '72em'], 3 | colors: { 4 | black: '#020502', 5 | primary: '#78f0a6', 6 | white: '#ffffff', 7 | gray: '#898ba3', 8 | }, 9 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512], 10 | sizes: [16, 32, 64, 80, 128, 192, 256, 288, 384, 512, 768, 1024, 1536], 11 | radii: [0, 8, 16, 9999, '100%'], 12 | fontSizes: [12, 14, 16, 20, 24, 36, 48, 80, 96], 13 | fontWeights: [100, 200, 300, 400, 500, 600, 700, 800, 900], 14 | fonts: { 15 | inter: '"Inter", sans-serif', 16 | sans: 17 | "-apple-system, BlinkMacSystemFont, 'avenir next', avenir, 'helvetica neue', helvetica, ubuntu, roboto, noto, 'segoe ui', arial, sans-serif", 18 | }, 19 | lineHeights: { 20 | solid: 1, 21 | title: 1.25, 22 | copy: 1.5, 23 | }, 24 | letterSpacings: { 25 | normal: 'normal', 26 | tracked: '0.1em', 27 | tight: '-0.05em', 28 | mega: '0.25em', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/useThemes.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import breathe from './imgs/fabian-moller-401625-unsplash.jpg'; 3 | import summer from './imgs/ernest-porzi-19106-unsplash.jpg'; 4 | import renaissance from './imgs/sandra-ollier-663002-unsplash.jpg'; 5 | import ayla from './imgs/ayla.jpg'; 6 | import paridhi from './imgs/paridhi.png'; 7 | import zainab from './imgs/zainab.png'; 8 | 9 | const variants = [ 10 | { 11 | media: { 12 | image: breathe, // Photo by Fabian Møller on Unsplash 13 | title: 'Loyal Community', 14 | body: 'How to build a loyal community for your brand.', 15 | }, 16 | profile: { 17 | avatar: paridhi, 18 | name: 'Paridhi Dutta', 19 | title: 'Community Manager', 20 | }, 21 | theme: { 22 | colors: { 23 | black: '#2d2733', 24 | primary: '#d675ff', 25 | secondary: '#f9e9fe', 26 | tertiary: '#e5a8ff', 27 | white: 'var(--material-white)', 28 | materialGray: '#898ba3', 29 | materialWhite: '#fff', 30 | gray: 'var(--material-gray)', 31 | }, 32 | radii: [0, 8, 16, 9999, '100%'], 33 | }, 34 | }, 35 | { 36 | media: { 37 | image: summer, // Photo by Ernest Porzi on Unsplash 38 | title: 'Summer Days', 39 | body: 'I am awakened by these beams of light.', 40 | }, 41 | profile: { 42 | avatar: zainab, 43 | name: 'Zainab Mian', 44 | title: 'UX Designer', 45 | }, 46 | theme: { 47 | colors: { 48 | black: '#272833', 49 | primary: '#6a74ff', 50 | secondary: '#e7e9ff', 51 | tertiary: '#98a2fe', 52 | white: 'var(--material-white)', 53 | materialGray: '#979bae', 54 | materialWhite: '#fff', 55 | gray: 'var(--material-gray)', 56 | }, 57 | radii: [0, 6, 16, 16, 9999, '100%'], 58 | }, 59 | }, 60 | { 61 | media: { 62 | image: renaissance, // Photo by Sandra Ollier on Unsplash 63 | title: 'Renaissance', 64 | body: 'Humanity: the center of interest.', 65 | }, 66 | profile: { 67 | avatar: ayla, 68 | name: 'Ayla Gauthier', 69 | title: 'Artist', 70 | }, 71 | theme: { 72 | colors: { 73 | black: '#000000', 74 | primary: '#5f7166', 75 | secondary: '#e4e8e6', 76 | tertiary: '#bbbb9f', 77 | white: 'var(--material-white)', 78 | materialGray: '#959e97', 79 | materialWhite: '#fff', 80 | gray: 'var(--material-gray)', 81 | }, 82 | radii: [0, 0, 16, 9999, '100%'], 83 | }, 84 | }, 85 | ]; 86 | 87 | export function useThemes() { 88 | const [activeIndex, setActiveIndex] = useState(1); 89 | const [theme, setTheme] = useState(variants[1]); 90 | 91 | return [ 92 | theme, 93 | activeIndex, 94 | index => { 95 | setActiveIndex(index); 96 | setTheme(variants[index]); 97 | }, 98 | ]; 99 | } 100 | --------------------------------------------------------------------------------