├── README.md ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── fonts.css ├── fonts │ ├── SourceSerif4-Black.ttf.woff │ └── SourceSerif4Variable-Roman.otf.woff ├── manifest.json └── index.html ├── src ├── App.js ├── Header.js ├── setupTests.js ├── App.test.js ├── Section.js ├── Number.js ├── css │ ├── index.css │ ├── Graph.scss │ ├── Demo.scss │ ├── Section.scss │ └── Viewport.scss ├── index.js ├── calculateValue.js ├── OutputCSS.js ├── PointList.js ├── Viewport.js ├── logo.svg ├── Demo.js ├── Graph.js ├── serviceWorker.js └── SampleText.js ├── .gitignore └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # A continuous approach to web typography -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomephant/continuous-typography/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomephant/continuous-typography/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomephant/continuous-typography/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Source Serif"; 3 | src: url("./fonts/SourceSerif4Variable-Roman.otf.woff") format("woff"); 4 | } 5 | -------------------------------------------------------------------------------- /public/fonts/SourceSerif4-Black.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomephant/continuous-typography/HEAD/public/fonts/SourceSerif4-Black.ttf.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerif4Variable-Roman.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awesomephant/continuous-typography/HEAD/public/fonts/SourceSerif4Variable-Roman.otf.woff -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Demo from "./Demo"; 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /src/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Header() { 4 | return ( 5 |
6 |

Continous web typography

7 |
8 | ); 9 | } 10 | 11 | export default Header; 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 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/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Section.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./css/Section.scss"; 3 | 4 | export default function Section(props) { 5 | 6 | return ( 7 |
8 |
9 |

{props.title}

10 |
11 |
{props.children}
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.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 | documentation 25 | -------------------------------------------------------------------------------- /src/Number.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Number(props) { 4 | function handleChange(e) { 5 | props.changeHandler(parseFloat(e.currentTarget.value)); 6 | } 7 | 8 | return ( 9 | 13 | ); 14 | } 15 | 16 | export default Number; 17 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-size: 100%; 4 | padding: 0; 5 | margin: 0; 6 | font-weight: inherit; 7 | color: inherit; 8 | -webkit-tap-highlight-color: transparent; 9 | } 10 | 11 | :root { 12 | --accent: rgb(228, 27, 0); 13 | --background: rgb(248, 248, 248); 14 | } 15 | 16 | body { 17 | font-family: sans-serif; 18 | height: 100vh; 19 | overflow: hidden; 20 | } 21 | 22 | button, 23 | input { 24 | font-family: inherit; 25 | color: inherit; 26 | } 27 | 28 | table { 29 | border-collapse: collapse; 30 | border-spacing: 0; 31 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './css/index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Continous Typography Tester 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/calculateValue.js: -------------------------------------------------------------------------------- 1 | function calculateValue(setting, x) { 2 | for (let i = 0; i < setting.points.length - 1; i++) { 3 | let currentPoint = setting.points[i] 4 | let nextPoint = setting.points[i + 1] 5 | 6 | if (currentPoint.x === x) { 7 | return currentPoint.y; 8 | } 9 | if (currentPoint.x < x && nextPoint.x > x) { 10 | // We've found the two points we're between 11 | let slope = (nextPoint.y - currentPoint.y) / (nextPoint.x - currentPoint.x) 12 | let y0 = currentPoint.y - slope * currentPoint.x; 13 | return y0 + x * slope; 14 | } 15 | } 16 | if (x > setting.points[setting.points.length - 1].x) { 17 | return setting.points[setting.points.length - 1].y; 18 | } 19 | return 0; 20 | } 21 | 22 | export default calculateValue; -------------------------------------------------------------------------------- /src/OutputCSS.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function OutputCSS(props) { 4 | const settingCSS = props.settings.map((setting) => { 5 | let rules = setting.points.map((p, i) => { 6 | let slope = 0; 7 | let yIntersection = 0; 8 | const nextPoint = setting.points[i + 1]; 9 | if (nextPoint) { 10 | slope = (nextPoint.y - p.y) / (nextPoint.x - p.x); 11 | yIntersection = p.y; 12 | return ` 13 | ${setting.rule}: calc(${yIntersection}${setting.unit} + ${ 14 | slope * props.viewportWidth 15 | }${setting.unit});`; 16 | } 17 | }); 18 | return rules.join(""); 19 | }); 20 | 21 | const css = ` 22 | .demo-paragraph p { 23 | ${settingCSS.join("")} 24 | } 25 | 26 | `; 27 | return ; 28 | } 29 | 30 | export default OutputCSS; 31 | -------------------------------------------------------------------------------- /src/css/Graph.scss: -------------------------------------------------------------------------------- 1 | .graph { 2 | svg { 3 | width: 100%; 4 | border: 1px solid var(--accent); 5 | margin-bottom: .5rem; 6 | cursor: crosshair; 7 | } 8 | circle { 9 | cursor: grab; 10 | fill: white; 11 | stroke-width: .5px; 12 | stroke: var(--accent); 13 | } 14 | polyline { 15 | stroke-width: .5px; 16 | stroke: var(--accent); 17 | fill: none; 18 | } 19 | .graph-x { 20 | stroke-width: .5px; 21 | opacity: .5; 22 | stroke: var(--accent); 23 | } 24 | .graph-value { 25 | fill: var(--accent); 26 | } 27 | .graph-background { 28 | fill: rgb(255, 255, 255); 29 | } 30 | .graph-output { 31 | background: var(--accent); 32 | color: white; 33 | border-radius: 3px; 34 | padding: .1em .3em; 35 | padding-top: 0; 36 | font-size: .9em; 37 | display: inline-block; 38 | margin-bottom: .5rem; 39 | } 40 | } 41 | .graph[data-dragging=true] circle { 42 | cursor: grabbing; 43 | } -------------------------------------------------------------------------------- /src/css/Demo.scss: -------------------------------------------------------------------------------- 1 | .demo { 2 | width: 100%; 3 | display: grid; 4 | grid-template-columns: 1fr 20rem; 5 | background: var(--background); 6 | } 7 | 8 | .demo-header { 9 | position: absolute; 10 | top: 1rem; 11 | left: 1rem; 12 | background: var(--accent); 13 | color: white; 14 | padding: 0.5rem; 15 | font-family: monospace; 16 | font-size: 16px; 17 | border-radius: 2px; 18 | } 19 | 20 | .result { 21 | height: 100vh; 22 | } 23 | .settings { 24 | position: absolute; 25 | width: 20rem; 26 | right: 1rem; 27 | top: 1rem; 28 | background: white; 29 | border-radius: 2px; 30 | overflow: hidden; 31 | border: 1px solid var(--accent); 32 | font-family: monospace; 33 | font-size: 16px; 34 | } 35 | .settings main { 36 | height: 90vh; 37 | overflow-y: scroll; 38 | } 39 | 40 | .panel-header { 41 | padding: 0.5rem; 42 | position: sticky; 43 | top: 0; 44 | background: var(--accent); 45 | color: white; 46 | border-bottom: 1px solid var(--accent); 47 | } 48 | -------------------------------------------------------------------------------- /src/css/Section.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | padding: 0.5rem; 3 | border-bottom: 1px solid var(--accent); 4 | z-index: 1000; 5 | color: var(--accent); 6 | &:last-of-type { 7 | border-bottom: 0; 8 | } 9 | } 10 | 11 | .section-header { 12 | color: var(--accent); 13 | padding-bottom: 0.5rem; 14 | } 15 | 16 | .number { 17 | display: flex; 18 | margin-bottom: 0.25rem; 19 | span { 20 | display: block; 21 | margin-right: 0.5rem; 22 | flex-basis: 3rem; 23 | flex-grow: 1; 24 | opacity: .5; 25 | } 26 | input { 27 | display: block; 28 | width: 5rem; 29 | appearance: textfield; 30 | background: transparent; 31 | border: none; 32 | text-decoration: underline; 33 | padding: 0 0.2rem; 34 | } 35 | } 36 | 37 | .point-delete { 38 | appearance: none; 39 | background: none; 40 | border: 0; 41 | justify-self: flex-end; 42 | margin-left: 1em; 43 | cursor: pointer; 44 | } 45 | 46 | .point { 47 | width: 100%; 48 | display: flex; 49 | justify-content: space-between; 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "continuous-typography", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://awesomephant.github.io/continuous-typography", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "gh-pages": "^3.1.0", 11 | "node-sass": "^4.14.1", 12 | "react": "^16.14.0", 13 | "react-dom": "^16.14.0", 14 | "react-scripts": "3.4.3" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d build" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PointList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Number from "./Number"; 3 | 4 | function PointList(props) { 5 | function updatePoint(i, key, value) { 6 | let newSetting = { ...props.setting }; 7 | newSetting.points[i][key] = value; 8 | props.updateSetting(newSetting); 9 | } 10 | 11 | function deletePoint(i) { 12 | let newSetting = { ...props.setting }; 13 | console.log(i) 14 | newSetting.points.splice(i, 1); 15 | props.updateSetting(newSetting); 16 | } 17 | 18 | const inputs = props.setting.points.map((p, i) => { 19 | let canDelete = true; 20 | if (i === 0 || i === props.setting.points.length - 1 ){ 21 | canDelete = false 22 | } 23 | return ( 24 |
  • 25 | 30 | 35 | {canDelete && 36 | 37 | } 38 |
  • 39 | ); 40 | }); 41 | 42 | return ( 43 | 44 | ); 45 | } 46 | 47 | export default PointList; 48 | -------------------------------------------------------------------------------- /src/Viewport.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import "./css/Viewport.scss"; 3 | function Viewport(props) { 4 | 5 | const [dragging, setDragging] = useState(false) 6 | const [x0, setX0] = useState(0) 7 | const [w0, setW0] = useState(0) 8 | const [xDelta, setXDelta] = useState(0) 9 | 10 | function handleMouseDown(index, e) { 11 | e.preventDefault() 12 | setDragging(index) 13 | setX0(e.clientX) 14 | setW0(props.width) 15 | } 16 | function handleMouseMove(e) { 17 | let sign = -1; 18 | if (dragging === "right") { 19 | sign = 1; 20 | } 21 | if (dragging){ 22 | setXDelta(e.clientX - x0); 23 | props.setViewportWidth(w0 + xDelta * (2 * sign)) 24 | } 25 | } 26 | 27 | function handleMouseUp() { 28 | setDragging(false) 29 | setX0(0) 30 | } 31 | 32 | return ( 33 |
    34 |
    35 |
    handleMouseDown("left", e)} onMouseUp={handleMouseUp} className="resize-handle left">
    36 |
    {props.children}
    37 |
    handleMouseDown("right", e)} onMouseUp={handleMouseUp} className="resize-handle right">
    38 |
    39 |
    40 | ); 41 | } 42 | 43 | export default Viewport; 44 | -------------------------------------------------------------------------------- /src/css/Viewport.scss: -------------------------------------------------------------------------------- 1 | .viewport-container { 2 | display: flex; 3 | align-items: space-around; 4 | width: 100%; 5 | height: 100%; 6 | } 7 | 8 | .viewport { 9 | margin: 0 auto; 10 | position: relative; 11 | top: 50%; 12 | transform: translateY(-50%); 13 | height: 80vh; 14 | .number { 15 | font-family: monospace; 16 | input { 17 | background: transparent; 18 | } 19 | } 20 | } 21 | 22 | .viewport-copy { 23 | padding: 2em 1.5em; 24 | height: 80vh; 25 | border-radius: 5px; 26 | background: white; 27 | border: 1px solid var(--accent); 28 | overflow-y: scroll; 29 | p { 30 | font-family: "Source Serif", sans-serif; 31 | margin: 0 auto; 32 | hyphens: auto; 33 | max-width: 32em; 34 | &:last-of-type { 35 | padding-bottom: 3em; 36 | } 37 | } 38 | p + p { 39 | text-indent: 3em; 40 | 41 | } 42 | } 43 | 44 | .viewport-copy strong { 45 | display: inline-block; 46 | } 47 | 48 | .viewport-width { 49 | position: absolute; 50 | top: -1.8em; 51 | left: 50%; 52 | transform: translateX(-50%); 53 | input { 54 | text-align: center; 55 | } 56 | } 57 | 58 | .resize-handle { 59 | background: white; 60 | border: 1px solid var(--accent); 61 | height: 100%; 62 | width: 15px; 63 | position: absolute; 64 | top: 0; 65 | transition: 100ms; 66 | border-radius: 2px; 67 | cursor: ew-resize; 68 | &:hover { 69 | } 70 | &.left { 71 | left: -25px; 72 | } 73 | &.right { 74 | right: -25px; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Demo.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Section from "./Section"; 3 | import Graph from "./Graph"; 4 | import Viewport from "./Viewport"; 5 | import PointList from "./PointList"; 6 | import Number from "./Number"; 7 | import calculateValue from "./calculateValue"; 8 | import SampleText from "./SampleText" 9 | 10 | import "./css/Demo.scss"; 11 | function Demo() { 12 | 13 | const [viewportWidth, setViewportWidth] = useState(window.innerWidth * .5); 14 | const [fontSize, setFontSize] = useState({ 15 | points: [ 16 | { x: 0, y: 15 }, 17 | { x: 400, y: 16.2 }, 18 | { x: 660, y: 18.7 }, 19 | { x: 960, y: 23.5 }, 20 | { x: 1400, y: 26.8 }, 21 | { x: 2000, y: 28 }, 22 | ], 23 | displayUnit: "px" 24 | }); 25 | const [lineHeight, setLineHeight] = useState({ 26 | points: [ 27 | { x: 0, y: 1.5 }, 28 | { x: 270, y: 1.42 }, 29 | { x: 700, y: 1.4 }, 30 | { x: 1100, y: 1.41 }, 31 | { x: 1400, y: 1.43 }, 32 | { x: 2000, y: 1.5 }, 33 | ], 34 | displayUnit: "" 35 | }); 36 | const [letterSpacing, setLetterSpacing] = useState({ 37 | points: [ 38 | { x: 0, y: .001 }, 39 | { x: 350, y: -.003 }, 40 | { x: 1200, y: -.005 }, 41 | { x: 2000, y: -.01 }, 42 | ], 43 | displayUnit: "em" 44 | }); 45 | const [opticalSize, setOpticalSize] = useState({ 46 | points: [ 47 | { x: 0, y: 8 }, 48 | { x: 140, y: 15.6 }, 49 | { x: 435, y: 22.1 }, 50 | { x: 960, y: 28.3 }, 51 | { x: 2000, y: 30 }, 52 | ], 53 | displayUnit: "" 54 | }); 55 | 56 | const demoStyle = { 57 | fontSize: `${calculateValue(fontSize, viewportWidth)}px`, 58 | lineHeight: `${calculateValue(lineHeight, viewportWidth)}`, 59 | letterSpacing: `${calculateValue(letterSpacing, viewportWidth)}em`, 60 | fontVariationSettings: `"opsz" ${calculateValue(opticalSize, viewportWidth)}` 61 | } 62 | 63 | return ( 64 |
    65 |
    Continuous Typography Tester (Information)
    66 |
    67 |
    68 |

    Settings

    69 |
    70 |
    71 |
    72 | 73 | 74 |
    75 |
    76 | 77 | 78 |
    79 |
    80 | 81 | 82 |
    83 |
    84 | 85 | 86 |
    87 |
    88 |
    89 |
    90 | 91 | 92 |
    93 | 94 |
    95 |
    96 |
    97 |
    98 | ); 99 | } 100 | 101 | export default Demo; 102 | -------------------------------------------------------------------------------- /src/Graph.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import calculateValue from "./calculateValue"; 3 | import "./css/Graph.scss"; 4 | 5 | function min(arr, key) { 6 | let min = 99999; 7 | arr.forEach((el) => { 8 | if (el[key] < min) { 9 | min = el[key]; 10 | } 11 | }); 12 | return min; 13 | } 14 | function max(arr, key) { 15 | let max = 0; 16 | arr.forEach((el) => { 17 | if (el[key] > max) { 18 | max = el[key]; 19 | } 20 | }); 21 | return max; 22 | } 23 | 24 | function Graph(props) { 25 | const h = 75; 26 | const w = 100; 27 | 28 | const minY = min(props.setting.points, "y"); 29 | const maxY = max(props.setting.points, "y"); 30 | const minX = min(props.setting.points, "x"); 31 | const maxX = max(props.setting.points, "x"); 32 | const svgEl = useRef(); 33 | const [draggingPoint, setDraggingPoint] = useState(false) 34 | 35 | function addPoint(i, point) { 36 | let newSetting = { ...props.setting }; 37 | newSetting.points.splice(i, 0, point); 38 | props.updateSetting(newSetting); 39 | } 40 | function updatePoint(i, point) { 41 | let newSetting = { ...props.setting }; 42 | newSetting.points[i] = point; 43 | props.updateSetting(newSetting); 44 | } 45 | 46 | function scaleY(value) { 47 | let normalised = (value - minY) / (maxY - minY); 48 | let scaled = h - normalised * (h); 49 | return scaled; 50 | } 51 | function scaleX(value) { 52 | let normalised = (value - minX) / (maxX - minX); 53 | let scaled = normalised * (w); 54 | return scaled; 55 | } 56 | 57 | function handlePointMouseDown(e, i) { 58 | setDraggingPoint(i) 59 | } 60 | function handlePointMouseUp(e, i) { 61 | setDraggingPoint(false); 62 | } 63 | function handleMouseMove(e) { 64 | if (draggingPoint !== false) { 65 | const normalX = (e.clientX - e.currentTarget.getBoundingClientRect().x) / e.currentTarget.getBoundingClientRect().width 66 | const x = Math.round(normalX * (maxX - minX)) 67 | 68 | const normalY = 1 - (e.clientY - e.currentTarget.getBoundingClientRect().y) / e.currentTarget.getBoundingClientRect().height 69 | const y = normalY * (maxY - minY) + minY 70 | 71 | updatePoint(draggingPoint, { x: x, y: y }) 72 | } 73 | } 74 | function handleClick(e) { 75 | // First we determine what the real x and y values for the new point are 76 | const normalX = (e.clientX - e.currentTarget.getBoundingClientRect().x) / e.currentTarget.getBoundingClientRect().width 77 | const x = Math.round(normalX * (maxX - minX)) 78 | 79 | const normalY = 1 - (e.clientY - e.currentTarget.getBoundingClientRect().y) / e.currentTarget.getBoundingClientRect().height 80 | const y = normalY * (maxY - minY) + minY 81 | 82 | // Then we find where the point should go in the array 83 | let index = 0; 84 | for (let i = 0; i < props.setting.points.length; i++) { 85 | if (props.setting.points[i].x > x) { 86 | index = i; 87 | break; 88 | } 89 | } 90 | 91 | let p = { 92 | x: x, 93 | y: y 94 | } 95 | addPoint(index, p) 96 | } 97 | 98 | const points = props.setting.points.map((p) => { 99 | const x = p.x; 100 | const y = p.y; 101 | return { x: scaleX(x), y: scaleY(y) }; 102 | }); 103 | 104 | const pointString = points.map((p) => { 105 | return `${p.x},${p.y} `; 106 | }); 107 | pointString.push(`${w}, ${points[points.length - 1].y} `) 108 | pointString.unshift(`${0}, ${points[0].y} `) 109 | 110 | const dots = props.setting.points.map((p, i) => { 111 | return handlePointMouseDown(e, i)} onMouseUp={(e) => handlePointMouseUp(e, i)} key={`dot-${i}`} cx={scaleX(p.x)} cy={scaleY(p.y)} r={1.5}>; 112 | }); 113 | return ( 114 |
    115 | 116 | 117 | 123 | 124 | {dots} 125 | 126 | 127 | 128 | {calculateValue(props.setting, props.viewportWidth).toFixed(3)}{props.setting.displayUnit} 129 | 130 |
    131 | ); 132 | } 133 | 134 | export default Graph; 135 | -------------------------------------------------------------------------------- /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.0/8 are 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 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/SampleText.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | function SampleText() { 4 | return ( 5 | <> 6 |

    7 | When we look to the individuals of the same variety or sub-variety of our older cultivated plants and animals, one of the first points which strikes us, is, that they generally differ much more from each other, than do the individuals of any one species or variety in a state of nature. When we reflect on the vast diversity of the plants and animals which have been cultivated, and which have varied during all ages under the most different climates and treatment, I think we are driven to conclude that this greater variability is simply due to our domestic productions having been raised under conditions of life not so uniform as, and somewhat different from, those to which the parent-species have been exposed under nature. There is, also, I think, some probability in the view propounded by Andrew Knight, that this variability may be partly connected with excess of food. It seems pretty clear that organic beings must be exposed during several generations to the new conditions of life to cause any appreciable amount of variation; and that when the organisation has once begun to vary, it generally continues to vary for many generations. No case is on record of a variable being ceasing to be variable under cultivation. Our oldest cultivated plants, such as wheat, still often yield new varieties: our oldest domesticated animals are still capable of rapid improvement or modification. 8 |

    9 |

    10 | It has been disputed at what period of life the causes of variability, whatever they may be, generally act; whether during the early or late period of development of the embryo, or at the instant of conception. Geoffroy St. Hilaire's experiments show that unnatural treatment of the embryo causes monstrosities; and monstrosities cannot be separated by any clear line of distinction from mere variations. But I am strongly inclined to suspect that the most frequent cause of variability may be attributed to the male and female reproductive elements having been affected prior to the act of conception. Several reasons make me believe in this; but the chief one is the remarkable effect which confinement or cultivation has on the functions of the reproductive system; this system appearing to be far more susceptible than any other part of the organisation, to the action of any change in the conditions of life. Nothing is more easy than to tame an animal, and few things more difficult than to get it to breed freely under confinement, even in the many cases when the male and female unite. How many animals there are which will not breed, though living long under not very close confinement in their native country! This is generally attributed to vitiated instincts; but how many cultivated plants display the utmost vigour, and yet rarely or never seed! In some few such cases it has been found out that very trifling changes, such as a little more or less water at some particular period of growth, will determine whether or not the plant sets a seed. I cannot here enter on the copious details which I have collected on this curious subject; but to show how singular the laws are which determine the reproduction of animals under confinement, I may just mention that carnivorous animals, even from the tropics, breed in this country pretty freely under confinement, with the exception of the plantigrades or bear family; whereas, carnivorous birds, with the rarest exceptions, hardly ever lay fertile eggs. Many exotic plants have pollen utterly worthless, in the same exact condition as in the most sterile hybrids. When, on the one hand, we see domesticated animals and plants, though often weak and sickly, yet breeding quite freely under confinement; and when, on the other hand, we see individuals, though taken young from a state of nature, perfectly tamed, long-lived, and healthy (of which I could give numerous instances), yet having their reproductive system so seriously affected by unperceived causes as to fail in acting, we need not be surprised at this system, when it does act under confinement, acting not quite regularly, and producing offspring not perfectly like their parents or variable. 11 |

    12 |

    13 | Sterility has been said to be the bane of horticulture; but on this view we owe variability to the same cause which produces sterility; and variability is the source of all the choicest productions of the garden. I may add, that as some organisms will breed most freely under the most unnatural conditions (for instance, the rabbit and ferret kept in hutches), showing that their reproductive system has not been thus affected; so will some animals and plants withstand domestication or cultivation, and vary very slightly—perhaps hardly more than in a state of nature. 14 |

    15 |

    16 | A long list could easily be given of "sporting plants;" by this term gardeners mean a single bud or offset, which suddenly assumes a new and sometimes very different character from that of the rest of the plant. Such buds can be propagated by grafting, &c., and sometimes by seed. These "sports" are extremely rare under nature, but far from rare under cultivation; and in this case we see that the treatment of the parent has affected a bud or offset, and not the ovules or pollen. But it is the opinion of most physiologists that there is no essential difference between a bud and an ovule in their earliest stages of formation; so that, in fact, "sports" support my view, that variability may be largely attributed to the ovules or pollen, or to both, having been affected by the treatment of the parent prior to the act of conception. These cases anyhow show that variation is not necessarily connected, as some authors have supposed, with the act of generation. 17 |

    18 |

    19 | Seedlings from the same fruit, and the young of the same litter, sometimes differ considerably from each other, though both the young and the parents, as Müller has remarked, have apparently been exposed to exactly the same conditions of life; and this shows how unimportant the direct effects of the conditions of life are in comparison with the laws of reproduction, and of growth, and of inheritance; for had the action of the conditions been direct, if any of the young had varied, all would probably have varied in the same manner. To judge how much, in the case of any variation, we should attribute to the direct action of heat, moisture, light, food, &c., is most difficult: my impression is, that with animals such agencies have produced very little direct effect, though apparently more in the case of plants. Under this point of view, Mr. Buckman's recent experiments on plants seem extremely valuable. When all or nearly all the individuals exposed to certain conditions are affected in the same way, the change at first appears to be directly due to such conditions; but in some cases it can be shown that quite opposite conditions produce similar changes of structure. Nevertheless some slight amount of change may, I think, be attributed to the direct action of the conditions of life—as, in some cases, increased size from amount of food, colour from particular kinds of food and from light, and perhaps the thickness of fur from climate. 20 |

    21 |

    22 | Habit also has a decided influence, as in the period of flowering with plants when transported from one climate to another. In animals it has a more marked effect; for instance, I find in the domestic duck that the bones of the wing weigh less and the bones of the leg more, in proportion to the whole skeleton, than do the same bones in the wild-duck; and I presume that this change may be safely attributed to the domestic duck flying much less, and walking more, than its wild parent. The great and inherited development of the udders in cows and goats in countries where they are habitually milked, in comparison with the state of these organs in other countries, is another instance of the effect of use. Not a single domestic animal can be named which has not in some country drooping ears; and the view suggested by some authors, that the drooping is due to the disuse of the muscles of the ear, from the animals not being much alarmed by danger, seems probable. 23 |

    24 | 25 | ) 26 | } 27 | 28 | export default SampleText; --------------------------------------------------------------------------------