├── package.json ├── src ├── App.module.css ├── index.js └── styles.css ├── README.md ├── LICENSE ├── .gitignore └── public └── index.html /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tensorflowjs-object-detection", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "@cloud-annotations/models": "^0.1.0", 9 | "react": "^16.12.0", 10 | "react-dom": "^16.12.0", 11 | "react-magic-dropzone": "^1.0.1", 12 | "react-scripts": "^3.3.1" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/App.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-direction: column; 6 | margin: 48px 16px 32px 16px; 7 | } 8 | 9 | .dropzoneBase { 10 | position: relative; 11 | padding: 12px; 12 | color: var(--detailText); 13 | border: 2px var(--textInputUnderline) dashed; 14 | border-radius: 5px; 15 | width: 100%; 16 | min-height: 144px; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | max-width: 500px; 21 | } 22 | 23 | .dropzone { 24 | composes: dropzoneBase; 25 | cursor: pointer; 26 | } 27 | 28 | .dropzone:hover { 29 | background: var(--highlight); 30 | } 31 | 32 | .image { 33 | width: 100%; 34 | height: 100%; 35 | border: thin solid rgba(64, 64, 64, 0.15); 36 | border-radius: 4px; 37 | object-fit: cover; 38 | } 39 | 40 | .canvas { 41 | position: absolute; 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Classification React App 2 | 3 | You can find an in depth walkthrough for training a TensorFlow.js model [here](https://github.com/cloud-annotations/training/). 4 | 5 | ## Setup 6 | `git clone` the repo and `cd` into it by running the following command: 7 | 8 | ```bash 9 | git clone https://github.com/cloud-annotations/classification-react.git 10 | cd classification-react 11 | ``` 12 | 13 | ### `npm install` 14 | 15 | > **Note: You’ll need to have Node 8.10.0 or later on your local development machine.** You can use [nvm](https://github.com/creationix/nvm#installation) (macOS/Linux) or [nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows) to easily switch Node versions between different projects. 16 | 17 | ## Add TensorFlow.js Model to the App 18 | Copy the `model_web` directory generated from the classification walkthrough and paste it into the `public` folder of this repo. 19 | 20 | ## Run the App 21 | ### `npm start` 22 | 23 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cloud Annotations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | model_web 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import MagicDropzone from "react-magic-dropzone"; 5 | import models from "@cloud-annotations/models"; 6 | 7 | import styles from "./App.module.css"; 8 | import "./styles.css"; 9 | 10 | const getRetinaContext = canvas => { 11 | const ctx = canvas.getContext("2d"); 12 | const scale = window.devicePixelRatio; 13 | let width = canvas.width / scale; 14 | let height = canvas.height / scale; 15 | return { 16 | setWidth: w => { 17 | width = w; 18 | canvas.style.width = w + "px"; 19 | canvas.width = w * scale; 20 | }, 21 | setHeight: h => { 22 | height = h; 23 | canvas.style.height = h + "px"; 24 | canvas.height = h * scale; 25 | }, 26 | width: width, 27 | height: height, 28 | clearAll: () => { 29 | return ctx.clearRect(0, 0, width * scale, height * scale); 30 | }, 31 | clearRect: (x, y, width, height) => { 32 | return ctx.clearRect(x * scale, y * scale, width * scale, height * scale); 33 | }, 34 | setFont: font => { 35 | const size = parseInt(font, 10) * scale; 36 | const retinaFont = font.replace(/^\d+px/, size + "px"); 37 | ctx.font = retinaFont; 38 | }, 39 | setTextBaseLine: textBaseline => { 40 | ctx.textBaseline = textBaseline; 41 | }, 42 | setStrokeStyle: strokeStyle => { 43 | ctx.strokeStyle = strokeStyle; 44 | }, 45 | setLineWidth: lineWidth => { 46 | ctx.lineWidth = lineWidth * scale; 47 | }, 48 | strokeRect: (x, y, width, height) => { 49 | return ctx.strokeRect( 50 | x * scale, 51 | y * scale, 52 | width * scale, 53 | height * scale 54 | ); 55 | }, 56 | setFillStyle: fillStyle => { 57 | ctx.fillStyle = fillStyle; 58 | }, 59 | measureText: text => { 60 | const metrics = ctx.measureText(text); 61 | return { 62 | width: metrics.width / scale, 63 | actualBoundingBoxLeft: metrics.actualBoundingBoxLeft / scale, 64 | actualBoundingBoxRight: metrics.actualBoundingBoxRight / scale, 65 | actualBoundingBoxAscent: metrics.actualBoundingBoxAscent / scale, 66 | actualBoundingBoxDescent: metrics.actualBoundingBoxDescent / scale 67 | }; 68 | }, 69 | fillRect: (x, y, width, height) => { 70 | return ctx.fillRect(x * scale, y * scale, width * scale, height * scale); 71 | }, 72 | fillText: (text, x, y) => { 73 | return ctx.fillText(text, x * scale, y * scale); 74 | } 75 | }; 76 | }; 77 | 78 | const renderObjectDetection = (ctx, predictions) => { 79 | ctx.clearAll(); 80 | // Font options. 81 | const font = `${14}px 'ibm-plex-sans', Helvetica Neue, Arial, sans-serif`; 82 | ctx.setFont(font); 83 | ctx.setTextBaseLine("top"); 84 | const border = 2; 85 | const xPadding = 8; 86 | const yPadding = 4; 87 | const offset = 2; 88 | const textHeight = parseInt(font, 10); // base 10 89 | 90 | predictions.forEach(prediction => { 91 | const x = prediction.bbox[0]; 92 | const y = prediction.bbox[1]; 93 | const width = prediction.bbox[2]; 94 | const height = prediction.bbox[3]; 95 | // Draw the bounding box. 96 | ctx.setStrokeStyle("#0062ff"); 97 | ctx.setLineWidth(border); 98 | 99 | ctx.strokeRect( 100 | Math.round(x), 101 | Math.round(y), 102 | Math.round(width), 103 | Math.round(height) 104 | ); 105 | // Draw the label background. 106 | ctx.setFillStyle("#0062ff"); 107 | const textWidth = ctx.measureText(prediction.label).width; 108 | ctx.fillRect( 109 | Math.round(x - border / 2), 110 | Math.round(y - (textHeight + yPadding) - offset), 111 | Math.round(textWidth + xPadding), 112 | Math.round(textHeight + yPadding) 113 | ); 114 | }); 115 | 116 | predictions.forEach(prediction => { 117 | const x = prediction.bbox[0]; 118 | const y = prediction.bbox[1]; 119 | // Draw the text last to ensure it's on top. 120 | ctx.setFillStyle("#ffffff"); 121 | ctx.fillText( 122 | prediction.label, 123 | Math.round(x - border / 2 + xPadding / 2), 124 | Math.round(y - (textHeight + yPadding) - offset + yPadding / 2) 125 | ); 126 | }); 127 | }; 128 | 129 | const renderClassification = (ctx, predictions) => { 130 | ctx.clearAll(); 131 | 132 | const font = `${14}px 'ibm-plex-sans', Helvetica Neue, Arial, sans-serif`; 133 | const textHeight = parseInt(font, 10); // base 10 134 | const xPadding = 8; 135 | const yPadding = 4; 136 | const offset = 2; 137 | ctx.setFont(font); 138 | ctx.setTextBaseLine("top"); 139 | 140 | predictions 141 | .filter(prediction => prediction.score > 0.5) 142 | .forEach((prediction, i) => { 143 | const label = `${prediction.label} ${(prediction.score * 100).toFixed( 144 | 1 145 | )}%`; 146 | // Draw the label background. 147 | ctx.setFillStyle("#0062ff"); 148 | const textWidth = ctx.measureText(label).width; 149 | ctx.fillRect( 150 | Math.round(xPadding), 151 | Math.round(xPadding + i * (textHeight + yPadding + offset)), 152 | Math.round(textWidth + xPadding), 153 | Math.round(textHeight + yPadding) 154 | ); 155 | // Draw the text last to ensure it's on top. 156 | ctx.setFillStyle("#ffffff"); 157 | ctx.fillText( 158 | label, 159 | Math.round(xPadding + 0 + xPadding / 2), 160 | Math.round( 161 | xPadding + i * (textHeight + yPadding + offset) + yPadding / 2 162 | ) 163 | ); 164 | }); 165 | }; 166 | 167 | const App = () => { 168 | const [model, setModel] = useState(undefined); 169 | const [preview, setPreview] = useState(undefined); 170 | const [resultsCanvas, setResultsCanvas] = useState(undefined); 171 | 172 | useEffect(() => { 173 | models.load("/model_web").then(async model => { 174 | // warm up the model 175 | const image = new ImageData(1, 1); 176 | if (model.type === "detection") { 177 | await model.detect(image); 178 | } else { 179 | await model.classify(image); 180 | } 181 | setModel(model); 182 | }); 183 | }, []); 184 | 185 | useEffect(() => { 186 | setPreview(undefined); 187 | if (resultsCanvas) { 188 | const ctx = getRetinaContext(resultsCanvas); 189 | ctx.clearAll(); 190 | ctx.setWidth(0); 191 | ctx.setHeight(0); 192 | } 193 | }, [model, resultsCanvas]); // if model changes kill preview. 194 | 195 | const onDrop = useCallback((accepted, _, links) => { 196 | setPreview(accepted[0].preview || links[0]); 197 | }, []); 198 | 199 | const onImageChange = useCallback( 200 | async e => { 201 | const imgWidth = e.target.clientWidth; 202 | const imgHeight = e.target.clientHeight; 203 | 204 | const ctx = getRetinaContext(resultsCanvas); 205 | ctx.setWidth(imgWidth); 206 | ctx.setHeight(imgHeight); 207 | 208 | if (model.type === "detection") { 209 | const predictions = await model.detect(e.target); 210 | renderObjectDetection(ctx, predictions); 211 | } else { 212 | const predictions = await model.classify(e.target); 213 | renderClassification(ctx, predictions); 214 | } 215 | }, 216 | [model, resultsCanvas] 217 | ); 218 | 219 | return ( 220 |
221 | 227 | {preview ? ( 228 |
229 | upload preview 235 |
236 | ) : model !== undefined ? ( 237 | "Drag & Drop an Image to Test" 238 | ) : ( 239 | "Loading model..." 240 | )} 241 | 242 |
243 |
244 | ); 245 | }; 246 | 247 | const rootElement = document.getElementById("root"); 248 | ReactDOM.render(, rootElement); 249 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | -webkit-font-smoothing: antialiased; 5 | overflow: hidden; 6 | font-family: "ibm-plex-sans", Helvetica Neue, Arial, sans-serif; 7 | /* text-rendering: optimizelegibility; */ 8 | background-color: hsl(210, 17%, 98%); 9 | color: rgba(0, 0, 0, 1); 10 | --detailText: rgba(0, 0, 0, 0.53); 11 | --textInput: #f3f3f3; 12 | --textInputUnderline: #a7a7a7; 13 | --highlight: #eef4fc; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | 20 | @font-face { 21 | font-family: "ibm-plex-mono"; 22 | font-style: normal; 23 | font-weight: 300; 24 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Light.woff") 25 | format("woff"); 26 | } 27 | 28 | @font-face { 29 | font-family: "ibm-plex-mono"; 30 | font-style: normal; 31 | font-weight: 300; 32 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Light-Pi.woff2") 33 | format("woff2"); 34 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 35 | } 36 | 37 | @font-face { 38 | font-family: "ibm-plex-mono"; 39 | font-style: normal; 40 | font-weight: 300; 41 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Light-Latin3.woff2") 42 | format("woff2"); 43 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 44 | } 45 | 46 | @font-face { 47 | font-family: "ibm-plex-mono"; 48 | font-style: normal; 49 | font-weight: 300; 50 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Light-Latin2.woff2") 51 | format("woff2"); 52 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 53 | } 54 | 55 | @font-face { 56 | font-family: "ibm-plex-mono"; 57 | font-style: normal; 58 | font-weight: 300; 59 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Light-Latin1.woff2") 60 | format("woff2"); 61 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 62 | } 63 | 64 | @font-face { 65 | font-family: "ibm-plex-mono"; 66 | font-style: normal; 67 | font-weight: 400; 68 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Regular.woff") 69 | format("woff"); 70 | } 71 | 72 | @font-face { 73 | font-family: "ibm-plex-mono"; 74 | font-style: normal; 75 | font-weight: 400; 76 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Regular-Pi.woff2") 77 | format("woff2"); 78 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 79 | } 80 | 81 | @font-face { 82 | font-family: "ibm-plex-mono"; 83 | font-style: normal; 84 | font-weight: 400; 85 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Regular-Latin3.woff2") 86 | format("woff2"); 87 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 88 | } 89 | 90 | @font-face { 91 | font-family: "ibm-plex-mono"; 92 | font-style: normal; 93 | font-weight: 400; 94 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Regular-Latin2.woff2") 95 | format("woff2"); 96 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 97 | } 98 | 99 | @font-face { 100 | font-family: "ibm-plex-mono"; 101 | font-style: normal; 102 | font-weight: 400; 103 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-Regular-Latin1.woff2") 104 | format("woff2"); 105 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 106 | } 107 | 108 | @font-face { 109 | font-family: "ibm-plex-mono"; 110 | font-style: normal; 111 | font-weight: 600; 112 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-SemiBold.woff") 113 | format("woff"); 114 | } 115 | 116 | @font-face { 117 | font-family: "ibm-plex-mono"; 118 | font-style: normal; 119 | font-weight: 600; 120 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-SemiBold-Pi.woff2") 121 | format("woff2"); 122 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 123 | } 124 | 125 | @font-face { 126 | font-family: "ibm-plex-mono"; 127 | font-style: normal; 128 | font-weight: 600; 129 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-SemiBold-Latin3.woff2") 130 | format("woff2"); 131 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 132 | } 133 | 134 | @font-face { 135 | font-family: "ibm-plex-mono"; 136 | font-style: normal; 137 | font-weight: 600; 138 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-SemiBold-Latin2.woff2") 139 | format("woff2"); 140 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 141 | } 142 | 143 | @font-face { 144 | font-family: "ibm-plex-mono"; 145 | font-style: normal; 146 | font-weight: 600; 147 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexMono-SemiBold-Latin1.woff2") 148 | format("woff2"); 149 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 150 | } 151 | 152 | @font-face { 153 | font-family: "ibm-plex-sans"; 154 | font-style: normal; 155 | font-weight: 300; 156 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Light.woff") 157 | format("woff"); 158 | } 159 | 160 | @font-face { 161 | font-family: "ibm-plex-sans"; 162 | font-style: normal; 163 | font-weight: 300; 164 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Light-Pi.woff2") 165 | format("woff2"); 166 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 167 | } 168 | 169 | @font-face { 170 | font-family: "ibm-plex-sans"; 171 | font-style: normal; 172 | font-weight: 300; 173 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Light-Latin3.woff2") 174 | format("woff2"); 175 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 176 | } 177 | 178 | @font-face { 179 | font-family: "ibm-plex-sans"; 180 | font-style: normal; 181 | font-weight: 300; 182 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Light-Latin2.woff2") 183 | format("woff2"); 184 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 185 | } 186 | 187 | @font-face { 188 | font-family: "ibm-plex-sans"; 189 | font-style: normal; 190 | font-weight: 300; 191 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Light-Latin1.woff2") 192 | format("woff2"); 193 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 194 | } 195 | 196 | @font-face { 197 | font-family: "ibm-plex-sans"; 198 | font-style: normal; 199 | font-weight: 400; 200 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Regular.woff") 201 | format("woff"); 202 | } 203 | 204 | @font-face { 205 | font-family: "ibm-plex-sans"; 206 | font-style: normal; 207 | font-weight: 400; 208 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Regular-Pi.woff2") 209 | format("woff2"); 210 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 211 | } 212 | 213 | @font-face { 214 | font-family: "ibm-plex-sans"; 215 | font-style: normal; 216 | font-weight: 400; 217 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Regular-Latin3.woff2") 218 | format("woff2"); 219 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 220 | } 221 | 222 | @font-face { 223 | font-family: "ibm-plex-sans"; 224 | font-style: normal; 225 | font-weight: 400; 226 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Regular-Latin2.woff2") 227 | format("woff2"); 228 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 229 | } 230 | 231 | @font-face { 232 | font-family: "ibm-plex-sans"; 233 | font-style: normal; 234 | font-weight: 400; 235 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-Regular-Latin1.woff2") 236 | format("woff2"); 237 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 238 | } 239 | 240 | @font-face { 241 | font-family: "ibm-plex-sans"; 242 | font-style: normal; 243 | font-weight: 600; 244 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-SemiBold.woff") 245 | format("woff"); 246 | } 247 | 248 | @font-face { 249 | font-family: "ibm-plex-sans"; 250 | font-style: normal; 251 | font-weight: 600; 252 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-SemiBold-Pi.woff2") 253 | format("woff2"); 254 | unicode-range: "U+03C0, U+0E3F, U+2070, U+2074-2079, U+2080-2089, U+2113, U+2116, U+2126, U+212E, U+2150-2151, U+2153-215E, U+2190-2199, U+21A9-21AA, U+21B0-21B3, U+21B6-21B7, U+21BA-21BB, U+21C4, U+21C6, U+2202, U+2206, U+220F, U+2211, U+221A, U+221E, U+222B, U+2248, U+2260, U+2264-2265, U+25CA, U+2713, U+274C, U+2B0E-2B11, U+EBE1, U+EBE3-EBE4, U+EBE6-EBE7, U+ECE0, U+EFCC"; 255 | } 256 | 257 | @font-face { 258 | font-family: "ibm-plex-sans"; 259 | font-style: normal; 260 | font-weight: 600; 261 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-SemiBold-Latin3.woff2") 262 | format("woff2"); 263 | unicode-range: "U+0102-0103, U+1EA0-1EF9, U+20AB"; 264 | } 265 | 266 | @font-face { 267 | font-family: "ibm-plex-sans"; 268 | font-style: normal; 269 | font-weight: 600; 270 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-SemiBold-Latin2.woff2") 271 | format("woff2"); 272 | unicode-range: "U+0100-024F, U+0259, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF, U+FB01-FB02"; 273 | } 274 | 275 | @font-face { 276 | font-family: "ibm-plex-sans"; 277 | font-style: normal; 278 | font-weight: 600; 279 | src: url("https://unpkg.com/carbon-components@latest/src/globals/fonts/IBMPlexSans-SemiBold-Latin1.woff2") 280 | format("woff2"); 281 | unicode-range: "U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FB01-FB02"; 282 | } 283 | 284 | @font-face { 285 | font-family: "ibm-plex-sans"; 286 | font-style: normal; 287 | font-weight: 500; 288 | src: url("https://raw.githubusercontent.com/IBM/plex/master/IBM-Plex-Sans/fonts/complete/woff/IBMPlexSans-Medium.woff") 289 | format("woff"); 290 | } 291 | 292 | @font-face { 293 | font-family: "ibm-plex-sans"; 294 | font-style: italic; 295 | font-weight: 500; 296 | src: url("https://raw.githubusercontent.com/IBM/plex/master/IBM-Plex-Sans/fonts/complete/woff/IBMPlexSans-MediumItalic.woff") 297 | format("woff"); 298 | } 299 | --------------------------------------------------------------------------------