├── .gitignore ├── CHANGE_LOG.md ├── README.md ├── change_log_images └── after-create-react-app-vscode-screenshot.png ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json ├── robots.txt └── youtube-talk-image.jpeg ├── react-create-README.md ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── components │ ├── ImageCanvas.tsx │ ├── PasteMessage.tsx │ └── UploadArea.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts └── utils │ ├── file-handling-utils.ts │ └── image-scaling-utils.ts ├── tsconfig.json └── yarn.lock /.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 | node_modules 25 | -------------------------------------------------------------------------------- /CHANGE_LOG.md: -------------------------------------------------------------------------------- 1 | ## 1) Starting Up the Project 2 | 3 | In the MacOS finder I made a folder called ReactConf-Lyle. 4 | 5 | > When I ran react-create (next step) - I got an error about the name having upper case. So in finder I changed the name to react-conf-2021-lyle 6 | 7 | I then Dropped the folder onto [Visual Studio Code](https://code.visualstudio.com/download) 8 | 9 | In Visual Studio Code I used `Ctrl+~` to open the terminal drawer at the bottom and then ran these commands. 10 | 11 | I like to have my git repo started first off. 12 | 13 | ```bash 14 | git init 15 | ``` 16 | 17 | Create a TypeScript React App 18 | 19 | ```bash 20 | npx create-react-app . --template typescript 21 | ``` 22 | 23 | Learn more about [create-react-app](https://create-react-app.dev/). 24 | 25 | > While this was running Visual Studio Code brought up a message about how git was having trouble keeping up and suggested that we add `/node_modules` to the project's [.gitignore](.gitignore) file. - I just said yes to this dialog. 26 | 27 | I decided that documenting things would be good so I made this file, and moved the react-create generated README.md file to be `react-create-README.md` - and then made a README.md for this project. 28 | 29 | create-react-app has a great return message... I went ahead and made a folder for keeping screenshots and [here is an image of what I got back](./change_log_images/after-create-react-app-vscode-screenshot.png) 30 | 31 | I then made the first commit by running these two commands: 32 | 33 | ``` 34 | git add . 35 | git commit -m "Initial Commit with React Create" 36 | ``` 37 | 38 | ## 2) Add a File Drop Area 39 | 40 | - Made an UploadArea component, and a utility for said. 41 | - Pulled the react logo 42 | - put some styling in the main css file - not happy with this - clean it up later? 43 | 44 | ## 3) Add the Canvas 45 | 46 | - Create an ImageCanvas Component that renders the image 47 | - Provide a toggle for Cover and Fit of the image 48 | 49 | ## 4) Start the On Paste handling 50 | 51 | - Create a PasteMessage Component 52 | - Add an event listener for `onpaste` 53 | - Add a "button" for using `navigator.clipboard` 54 | - Ensure we always use HTTPS 55 | - Small main page layout cleanup 56 | 57 | ## 5) Add Links 58 | 59 | - In the talk I mention a bunch of things - it seems like having these things all in one place would be useful. 60 | - Add React Conf and logo to the app 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Conf 2021 Talk 2 | 3 | ## UI Tools for Artists 4 | 5 | This repo was created to share source code for my (Lyle) December 8th 2021 React Conf lighting talk. 6 | 7 | Here is the video of my talk: 8 | [![Video of UI Tools for Artists React Talk](./public/youtube-talk-image.jpeg)](https://www.youtube.com/watch?v=b3l4WxipFsE&list=PLNG_1j3cPCaZZ7etkzWA7JfdmKWT0pMsa&t=1s) 9 | 10 | Here are some of the things I referred to in the talk: 11 | 12 | - My Netflix Culture Podcast [WeAreNetflix](https://jobs.netflix.com/podcast) 13 | - The wonderful Minion based [Animation Pipeline interactive site](https://www.illuminationMacGuff.com/pipeline) by Illumination MacGuff 14 | - Netflix Shows: 15 | - - [Over the Moon](https://www.netflix.com/title/80214236) - AWN's [article on OTM](https://www.awn.com/animationworld/glen-keanes-over-moon-artfully-illustrates-healing-grief-through-play) by Victoria Davis 16 | - - [Klaus](https://www.netflix.com/title/80183187) - IAMAG's article [The Art of Klaus](https://www.iamag.co/the-art-of-klaus-40-concept-art-collection/) 17 | - The [Hawkins: Diving into the Reasoning Behind our Design System](https://netflixtechblog.com/hawkins-diving-into-the-reasoning-behind-our-design-system-964a7357547) blog post by [Joshua Godi](https://www.linkedin.com/in/jgodi/) 18 | - The Art Director of SPA Studios [Szymon Biernacki](https://www.artstation.com/biernac) - the Snow Covered [The Post Office](https://www.artstation.com/artwork/PegLZ) 19 | 20 | The [photos I used](https://photos.app.goo.gl/ZpD3N3tThVL7ocodA) to show off different workshops and art studios were taken by me, of either my workshop, and workshops/studios I have visited, mostly while enjoying the [Santa Cruz Open Studios Art Tour](https://santacruzopenstudios.com/) which happens every year in October. 21 | 22 | I also found, but did not use, this amazing short film [Isolation](https://www.youtube.com/watch?v=v7_foJZk9zA) which shows a lot of different art studios. 23 | 24 | ## Running this Repo 25 | 26 | Download the source, open a terminal to the source folder and ... 27 | 28 | ``` 29 | yarn install 30 | yarn start 31 | ``` 32 | 33 | ## How I got Here 34 | 35 | With an attempt to document the full creation of this project I am documenting each step. You can see those steps in the [CHANGE_LOG](CHANGE_LOG.md) (and in the commit messages) 36 | -------------------------------------------------------------------------------- /change_log_images/after-create-react-app-vscode-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyle/react-conf-2021/54869b4192f4a008faccc85db23ef3f234fc91fc/change_log_images/after-create-react-app-vscode-screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-conf-2021-lyle", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^17.0.0", 12 | "@types/react-dom": "^17.0.0", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-scripts": "4.0.3", 16 | "typescript": "^4.1.2", 17 | "web-vitals": "^1.0.1" 18 | }, 19 | "scripts": { 20 | "start": "HTTPS=true react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyle/react-conf-2021/54869b4192f4a008faccc85db23ef3f234fc91fc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Conf 2021", 3 | "name": "Artists Flexibility", 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/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/youtube-talk-image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyle/react-conf-2021/54869b4192f4a008faccc85db23ef3f234fc91fc/public/youtube-talk-image.jpeg -------------------------------------------------------------------------------- /react-create-README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | 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. 37 | 38 | 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. 39 | 40 | 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. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | .App-logo { 25 | height: 2em; 26 | } 27 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./App.css"; 3 | import { ImageCanvas } from "./components/ImageCanvas"; 4 | import { PasteMessage } from "./components/PasteMessage"; 5 | import { UploadArea } from "./components/UploadArea"; 6 | import { ScaleType } from "./utils/image-scaling-utils"; 7 | import logo from "./logo.svg"; 8 | const WIDTH = 600; 9 | const HEIGHT = 400; 10 | function App() { 11 | const [image, setImage] = useState(); 12 | const [scaleType, setScaleType] = useState(ScaleType.COVER); 13 | 14 | return ( 15 |
16 |
17 |

18 | logo React Conf 2021 19 |

20 |

UI Tools for Artists

21 | 32 | 38 |
39 | { 41 | setImage(img); 42 | }} 43 | > 44 | { 46 | setImage(img); 47 | }} 48 | /> 49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/components/ImageCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useRef, useEffect } from "react"; 2 | import { drawCenter, ScaleType } from "../utils/image-scaling-utils"; 3 | 4 | interface ImageCanvasProps { 5 | image?: HTMLImageElement; 6 | widthTarget: number; 7 | heightTarget: number; 8 | scaleType: ScaleType; 9 | } 10 | export const ImageCanvas: FC = ({ 11 | image, 12 | widthTarget, 13 | heightTarget, 14 | scaleType, 15 | }) => { 16 | const canvasRef = useRef(null); 17 | const canvasCtxRef = useRef(null); 18 | useEffect(() => { 19 | if (canvasRef.current && !canvasCtxRef.current) { 20 | canvasCtxRef.current = canvasRef.current.getContext("2d"); 21 | } 22 | return () => { 23 | canvasRef.current = null; 24 | canvasCtxRef.current = null; 25 | }; 26 | }, []); 27 | useEffect(() => { 28 | if (!canvasCtxRef.current || !image) { 29 | return; 30 | } 31 | canvasCtxRef.current.clearRect(0, 0, widthTarget, heightTarget); 32 | drawCenter({ 33 | ctx: canvasCtxRef.current, 34 | image, 35 | heightTarget, 36 | widthTarget, 37 | coverageType: scaleType, 38 | }); 39 | }, [image, heightTarget, widthTarget, scaleType]); 40 | return ( 41 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/PasteMessage.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useState, useEffect, ReactElement } from "react"; 2 | 3 | type PasteMessageProps = { 4 | handleImage: (image: HTMLImageElement) => void; 5 | }; 6 | export const PasteMessage: FC = ({ handleImage }) => { 7 | const [errorMessage, setErrorMessage] = useState(); 8 | 9 | useEffect(() => { 10 | const pasteListener = function (event: ClipboardEvent) { 11 | const firstFile = event.clipboardData?.files?.item(0); 12 | if (firstFile) { 13 | const image = new Image(); 14 | image.onload = () => { 15 | handleImage(image); 16 | }; 17 | image.src = URL.createObjectURL(firstFile); 18 | } 19 | }; 20 | document.addEventListener("paste", pasteListener); 21 | return () => { 22 | document.removeEventListener("paste", pasteListener); 23 | }; 24 | }, [handleImage]); 25 | 26 | const getImage = useCallback(() => { 27 | if (!navigator.clipboard) { 28 | setErrorMessage( 29 |
30 | Sorry, your browser does not support this feature. 31 |
But you can just Paste.
32 |
33 | ); 34 | return; 35 | } else { 36 | setErrorMessage(undefined); 37 | } 38 | 39 | navigator.clipboard.read().then((clipboardItems: ClipboardItem[]) => { 40 | const clipboardItem = clipboardItems.find((item) => 41 | item.types.includes("image/png") 42 | ); 43 | if (clipboardItem) { 44 | clipboardItem.getType("image/png").then((blob) => { 45 | const image = new Image(); 46 | image.onload = () => { 47 | handleImage(image); 48 | }; 49 | image.src = URL.createObjectURL(blob); 50 | }); 51 | } 52 | }); 53 | }, [handleImage]); 54 | return ( 55 |
56 | {errorMessage ? ( 57 |
{errorMessage}
58 | ) : ( 59 |
image from clipboard
60 | )} 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/UploadArea.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useState, useEffect, useMemo } from "react"; 2 | import { filterFilesByValidExtensions } from "../utils/file-handling-utils"; 3 | const VALID_IMAGE_FILE_EXT = ["jpeg", "jpg", "png"]; 4 | type UploadAreaProps = { 5 | handleImage: (image: HTMLImageElement) => void; 6 | }; 7 | export const UploadArea: FC = ({ handleImage }) => { 8 | const [focusMe, setFocusMe] = useState(false); 9 | const [tooManyFiles, setTooManyFiles] = useState(false); 10 | const [images, setImages] = useState(); 11 | const handleDrop = useCallback( 12 | (event: React.DragEvent) => { 13 | event.preventDefault(); 14 | setFocusMe(false); 15 | if (tooManyFiles) { 16 | setTooManyFiles(false); 17 | return; 18 | } 19 | if (event.dataTransfer.items) { 20 | setImages( 21 | filterFilesByValidExtensions( 22 | event.dataTransfer.items, 23 | VALID_IMAGE_FILE_EXT 24 | ) 25 | ); 26 | } 27 | }, 28 | [tooManyFiles] 29 | ); 30 | useEffect(() => { 31 | if (images && images.length) { 32 | const image = new Image(); 33 | image.onload = () => { 34 | handleImage(image); 35 | }; 36 | image.src = URL.createObjectURL(images[0]); 37 | } 38 | }, [handleImage, images]); 39 | const additionalClassName = useMemo(() => { 40 | return focusMe && (tooManyFiles ? "error" : "focused"); 41 | }, [focusMe, tooManyFiles]); 42 | return ( 43 |
) => { 46 | event.preventDefault(); 47 | }} 48 | onDragEnter={(event: React.DragEvent) => { 49 | event.preventDefault(); 50 | setFocusMe(true); 51 | if (event.dataTransfer.items) { 52 | setTooManyFiles(event.dataTransfer.items.length > 1); 53 | } 54 | }} 55 | onDragLeave={(event) => { 56 | event.preventDefault(); 57 | setFocusMe(false); 58 | setTooManyFiles(false); 59 | }} 60 | onDrop={handleDrop} 61 | > 62 | {tooManyFiles ? "only one image please" : "drop an image here"} 63 |
64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /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 | .react-conf { 15 | display: flex; 16 | align-items: center; 17 | margin: 0; 18 | } 19 | .actions { 20 | display: flex; 21 | flex-direction: row; 22 | gap: 1em; 23 | padding-top: 1em; 24 | align-items: self-end; 25 | } 26 | 27 | .drop-zone, 28 | .paste-message { 29 | padding: 2em; 30 | border: 2px gray solid; 31 | border-radius: 20%; 32 | } 33 | .paste-message { 34 | justify-items: flex-end; 35 | } 36 | .paste-message:hover { 37 | border-color: black; 38 | cursor: pointer; 39 | background-color: rgba(200, 200, 200, 0.5); 40 | } 41 | .focused { 42 | background-color: rgba(200, 200, 200, 0.5); 43 | } 44 | .error { 45 | background-color: rgba(255, 0, 0, 0.5); 46 | } 47 | 48 | .our-canvas { 49 | border: 1px solid gray; 50 | } 51 | 52 | .toggle-button { 53 | background-color: transparent; 54 | border: none; 55 | color: gray; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/file-handling-utils.ts: -------------------------------------------------------------------------------- 1 | export const filterFilesByValidExtensions = ( 2 | files: DataTransferItemList, 3 | fileExtensions: string[] 4 | ) => { 5 | const allFiles = Array.from(files) 6 | .map((item) => item.getAsFile()) 7 | .filter((file) => !!file) as File[]; 8 | return allFiles.filter((file) => { 9 | const ext = file.name.split(".").pop()?.toLocaleLowerCase(); 10 | return ext ? fileExtensions.includes(ext) : false; 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/image-scaling-utils.ts: -------------------------------------------------------------------------------- 1 | interface CoverCenterProps { 2 | coverageType: ScaleType; 3 | ctx: CanvasRenderingContext2D; 4 | image: HTMLImageElement; 5 | heightTarget: number; 6 | widthTarget: number; 7 | } 8 | export enum ScaleType { 9 | COVER, 10 | FIT, 11 | } 12 | export const drawCenter = ({ 13 | ctx, 14 | image, 15 | heightTarget, 16 | widthTarget, 17 | coverageType, 18 | }: CoverCenterProps) => { 19 | const targetAspectRatio = widthTarget / heightTarget; 20 | const imageAspectRation = image.naturalWidth / image.naturalHeight; 21 | const imageMorePortrait = 0 > imageAspectRation - targetAspectRatio; 22 | let resizedWidth, resizedHeight, startX, startY; 23 | 24 | const shouldWidthMatch = 25 | coverageType === ScaleType.COVER ? imageMorePortrait : !imageMorePortrait; 26 | if (shouldWidthMatch) { 27 | resizedWidth = widthTarget; 28 | resizedHeight = widthTarget / imageAspectRation; 29 | startX = 0; 30 | startY = (heightTarget - resizedHeight) / 2; 31 | } else { 32 | resizedWidth = heightTarget * imageAspectRation; 33 | resizedHeight = heightTarget; 34 | startX = (widthTarget - resizedWidth) / 2; 35 | startY = 0; 36 | } 37 | ctx.drawImage(image, startX, startY, resizedWidth, resizedHeight); 38 | }; 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------