🎶 initializing with everything done
32
components/AnimatedNumber.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { useSpring, animated, config } from 'react-spring'
|
||||
import useOnScreen from '../lib/useOnScreen'
|
||||
import { useRef } from 'react'
|
||||
|
||||
const AnimatedNumber = ({ delay = 0, children }) => {
|
||||
const ref = useRef()
|
||||
const onScreen = useOnScreen(ref, '0px')
|
||||
const numbers = children.split("")
|
||||
const results = numbers.map((item, i) => {
|
||||
if (!parseInt(item)) {
|
||||
return item
|
||||
}
|
||||
const animation = useSpring({
|
||||
number: parseInt(item),
|
||||
from: {
|
||||
number: 0
|
||||
},
|
||||
config: { tension: 40, friction: 20, precision: 0.1 },
|
||||
delay,
|
||||
reset: onScreen,
|
||||
reverse: !onScreen
|
||||
})
|
||||
return (
|
||||
<animated.span key={`animated-number-${i}`} ref={ref} style={animation}>
|
||||
{ animation.number.interpolate(x => x.toFixed()) }
|
||||
</animated.span>
|
||||
)
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
export default AnimatedNumber
|
49
components/ArabicExample.js
Normal file
@ -0,0 +1,49 @@
|
||||
import styled from "@emotion/styled"
|
||||
import { useSpring, useTransition, useChain, animated, config } from 'react-spring'
|
||||
import useOnScreen from '../lib/useOnScreen'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
const Container = styled('div')`
|
||||
margin: 3rem auto;
|
||||
pre {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 5rem auto 1rem;
|
||||
max-width: 740px
|
||||
}
|
||||
`
|
||||
const Kashida = styled(animated.h1)`
|
||||
text-align: center;
|
||||
font-feature-settings: "salt";
|
||||
line-height: 1.6;
|
||||
`
|
||||
|
||||
export default () => {
|
||||
const ref = useRef()
|
||||
const onScreen = useOnScreen(ref, '0px')
|
||||
const [reversed, setReversed] = useState(false)
|
||||
|
||||
const animation = useSpring({
|
||||
lxnd: 0,
|
||||
from: {
|
||||
lxnd: 100
|
||||
},
|
||||
reset: onScreen,
|
||||
reverse: onScreen && reversed,
|
||||
onRest: () => onScreen && setReversed(!reversed)
|
||||
})
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Kashida ref={ref} style={{
|
||||
fontVariationSettings: animation.lxnd.interpolate(x => `'LXND' ${x.toFixed()}`)
|
||||
}}>
|
||||
a
|
||||
</Kashida>
|
||||
<pre>
|
||||
<animated.code>{animation.lxnd.interpolate(x => `h1, p { font-variation-settings: 'LXND' ${x.toFixed()}; }`)}</animated.code>
|
||||
</pre>
|
||||
<h6>An illustration of potential for using this concept for Arabic typography</h6>
|
||||
</Container>
|
||||
)
|
||||
}
|
140
components/Chart.js
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
import { useContext } from 'react'
|
||||
import TypeChoicesContext from '../lib/useTypeChoices'
|
||||
import useMediaQuery from '../lib/useMediaQuery'
|
||||
import styled from "@emotion/styled";
|
||||
import { ResponsiveBar } from "@nivo/bar";
|
||||
|
||||
import data from "../data/TNRvsLXND";
|
||||
|
||||
const Charts = () => (
|
||||
<Container>
|
||||
<BarChart />
|
||||
<h6>The p value in this experiment (0.014) says the chance of the results presented happening by random was 1.4%. In statistics, a result is considered significant if it’s below 5%.</h6>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const StudentNumber = styled('h6')`
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin: 2px 1px 3px;
|
||||
color: ${props => props.color};
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
`
|
||||
|
||||
const Tooltip = ({ id, value, color, indexValue: student, ...rest }) => (
|
||||
<>
|
||||
<StudentNumber color={color}>Student {student}</StudentNumber>
|
||||
<strong style={{ color }}>
|
||||
{id} @ {value} wpm
|
||||
</strong>
|
||||
</>
|
||||
);
|
||||
|
||||
const BarChart = () => {
|
||||
const { family, setTypeFamily } = useContext(TypeChoicesContext)
|
||||
const orientation = useMediaQuery(
|
||||
// Media queries
|
||||
['(max-width: 960px)'],
|
||||
['horizontal'],
|
||||
'vertical'
|
||||
);
|
||||
const orientationLabels = useMediaQuery(
|
||||
// Media queries
|
||||
['(max-width: 960px)'],
|
||||
[{
|
||||
left: "Anonymous Student #",
|
||||
bottom: 'Correct Words per Minute'
|
||||
}],
|
||||
{
|
||||
bottom: "Anonymous Student #",
|
||||
left: 'Correct Words per Minute'
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponsiveBar
|
||||
onMouseEnter={({ id }) => setTypeFamily(id)}
|
||||
data={data}
|
||||
keys={["Times New Roman", "Lexend"]}
|
||||
indexBy="Student"
|
||||
margin={{ top: 80, right: 20, bottom: 50, left: 47 }}
|
||||
padding={0.24}
|
||||
groupMode="grouped"
|
||||
layout={ orientation }
|
||||
enableGridX
|
||||
tooltip={Tooltip}
|
||||
colors={
|
||||
[
|
||||
'hsla(0, 0%, 85%, 1)',
|
||||
'rgba(250, 76, 76, 1)'
|
||||
]
|
||||
}
|
||||
legends={[
|
||||
{
|
||||
dataFrom: "keys",
|
||||
anchor: "top-right",
|
||||
direction: "column",
|
||||
justify: false,
|
||||
translateX: -40,
|
||||
translateY: 10,
|
||||
itemsSpacing: 2,
|
||||
itemWidth: 100,
|
||||
itemHeight: 20,
|
||||
itemDirection: "left-to-right",
|
||||
itemOpacity: 0.65,
|
||||
symbolSize: 20,
|
||||
effects: [
|
||||
{
|
||||
on: "hover",
|
||||
style: {
|
||||
itemOpacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
fontFamily="Lexend"
|
||||
borderColor={{ from: "color", modifiers: [["darker", 1.6]] }}
|
||||
axisLeft={{
|
||||
legend: orientationLabels.left,
|
||||
legendPosition: 'middle',
|
||||
legendOffset: -40
|
||||
}}
|
||||
axisBottom={{
|
||||
legend: orientationLabels.bottom,
|
||||
legendPosition: 'middle',
|
||||
legendOffset: 40
|
||||
}}
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
labelSkipWidth={12}
|
||||
labelSkipHeight={12}
|
||||
labelTextColor={{ from: "color", modifiers: [["darker", 0]] }}
|
||||
animate={true}
|
||||
motionStiffness={90}
|
||||
motionDamping={15}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled("section")`
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
padding-bottom: 2rem;
|
||||
h6 {
|
||||
font-size: 70%;
|
||||
line-height: 1.6;
|
||||
max-width: 88%;
|
||||
margin: 1.392rem auto 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
g {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Charts;
|
108
components/ChartStats.js
Normal file
@ -0,0 +1,108 @@
|
||||
import data from '../data/TNRvsLXND'
|
||||
import styled from "@emotion/styled"
|
||||
import AnimatedNumber from '../components/AnimatedNumber'
|
||||
const avg = arr => arr.reduce((a,b) => a + b, 0) / arr.length
|
||||
const max = arr => Math.max(...arr);
|
||||
|
||||
|
||||
const Stats = ({ children = [] }) => {
|
||||
return (
|
||||
<StatsContainer>
|
||||
{ children.map( (stat, i) => <AnimatedStat key={`stat-${i}`} index={i}>{stat}</AnimatedStat> )}
|
||||
</StatsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const AnimatedStat = ({ index, children }) => {
|
||||
// const regex = /([+|-])?(\d+)(?:\.\d+)?([\/|%])?/gm
|
||||
const regex = /([+|-])?((\d+[\/\d.]*|\d))([\/|%])?\s(.+)/gm
|
||||
let [matched, plusminus, number, _, percent, label ] = children.props.children.split(regex)
|
||||
return (
|
||||
<Stat key={`stat-${index}-${number}`}>
|
||||
<h5>
|
||||
<span className='extra'>{plusminus}</span>
|
||||
<AnimatedNumber delay={index}>{number}</AnimatedNumber>
|
||||
<span className='extra'>{percent}</span>
|
||||
</h5>
|
||||
|
||||
<p>
|
||||
{label}
|
||||
</p>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const StatsContainer = styled('section')`
|
||||
width: 98%;
|
||||
margin: 1rem auto;
|
||||
border: 1px solid red;
|
||||
padding: 0.192rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/* display: grid;
|
||||
grid-gap: 0.192rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); */
|
||||
`
|
||||
const Stat = styled('div')`
|
||||
position: relative;
|
||||
border: 1px solid red;
|
||||
padding: 3rem 0.618rem;
|
||||
text-align: center;
|
||||
flex: 1 1 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin: 0.192rem;
|
||||
p {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
color: rgba(0,0,0,0.7);
|
||||
font-size: 0.618rem;
|
||||
font-variation-settings: 'LXND' 80;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h5, strong {
|
||||
font-variation-settings: 'LXND' 0;
|
||||
display: block;
|
||||
font-size: 4.8vw;
|
||||
font-weight: normal;
|
||||
margin: 0.392rem 0;
|
||||
line-height: 1.2;
|
||||
em {
|
||||
font-size: 30%;
|
||||
vertical-align: super;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
margin: 0 0.618rem;
|
||||
font-style: normal;
|
||||
}
|
||||
.extra {
|
||||
font-size: 40%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
&:nth-child(1) {
|
||||
grid-column: span 2;
|
||||
min-width: 50%;
|
||||
h5 {
|
||||
font-size: 10vw;
|
||||
@media (max-width: 960px) {
|
||||
font-size: 12vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:nth-child(10) {
|
||||
grid-column: span 2;
|
||||
min-width: 50%;
|
||||
h5 {
|
||||
font-size: 10vw;
|
||||
@media (max-width: 960px) {
|
||||
font-size: 12vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default Stats
|
116
components/EmailForm.js
Normal file
@ -0,0 +1,116 @@
|
||||
import { useSpring, animated } from 'react-spring'
|
||||
import styled from "@emotion/styled"
|
||||
import { Button } from '../components'
|
||||
import useHover from '../lib/useHover'
|
||||
|
||||
const Box = styled('div')`
|
||||
border: 1px solid rgba(0,0,0,0.24);
|
||||
background: red;
|
||||
padding: 3px;
|
||||
margin: 5rem 0 6rem;
|
||||
`
|
||||
|
||||
const FormContainer = styled(animated.form)`
|
||||
box-shadow: 0px 0.392rem 0.618rem 0 rgba(0,0,0,0.12), 0px 0.618rem 1rem 0 rgba(0,0,0,0.07);
|
||||
border: 0.392rem solid red;
|
||||
padding: 1rem 3rem;
|
||||
max-width: 700px;
|
||||
width: 90%;
|
||||
margin: -3rem auto;
|
||||
background: white;
|
||||
div {
|
||||
display: block;
|
||||
margin: 2.618rem auto;
|
||||
}
|
||||
label {
|
||||
font-size: 0.718rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
display: block;
|
||||
font-variation-settings: 'LXND' 40;
|
||||
margin: 0 0.392rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=text], input[type=email] {
|
||||
margin: 0.618rem 0 0.618rem;
|
||||
font-family: "Lexend", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 1.8rem;
|
||||
-webkit-appearance: none;
|
||||
border: 0px;
|
||||
border-bottom: 1px dashed rgba(0,0,0,0.24);
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px dashed rgba(0,0,0,1);
|
||||
}
|
||||
}
|
||||
input[type=checkbox] {
|
||||
margin-right: 0.618rem;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
button {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
margin-top: 0.618rem;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
const Form = ({ children, ...props }) => {
|
||||
const [hoverRef, hovering] = useHover()
|
||||
|
||||
const animation = useSpring({
|
||||
transform: hovering ? 'translate3d(0,0,0) scale(1.024) rotate(0deg)' : 'translate3d(0,0,0) scale(1) rotate(0deg)',
|
||||
config: {
|
||||
mass: 0.24,
|
||||
tension: 200,
|
||||
friction: 5
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<FormContainer
|
||||
ref={hoverRef}
|
||||
style={animation}
|
||||
{...props}>
|
||||
{children}
|
||||
</FormContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const EmailForm = props => {
|
||||
return (
|
||||
<Box>
|
||||
<Form action="https://mailthis.to/info@lexend.com" method="POST">
|
||||
<input type="hidden" name="_subject" value="Lexend form submission"/>
|
||||
<input type="hidden" name="_after" value="http://www.lexend.com/"/>
|
||||
<input type="hidden" name="_honeypot" value=""/>
|
||||
<input type="hidden" name="_confirmation" value="Excellent! We'll be in touch once there are updates."/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name">Your Name:</label>
|
||||
<input type="text" name="name" id="name" placeholder="Sherlock"/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email">Your Email:</label>
|
||||
<input type="email" name="email" id="email" placeholder="sh.holmes@science.net"/>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="contribute"/>
|
||||
I'd love to help contribute
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Button value="submit">Keep me updated, send me details</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmailForm
|
55
components/Footer.js
Normal file
72
components/Highlighter.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { useContext } from 'react'
|
||||
import TypeChoicesContext from '../lib/useTypeChoices'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ms from 'ms'
|
||||
import parseMs from 'parse-ms'
|
||||
import prettyMs from 'pretty-ms'
|
||||
import styled, { css } from '@emotion/styled'
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
|
||||
const Highlighted = styled('strong')`
|
||||
background: yellow;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
`
|
||||
const Striked = styled('del')`
|
||||
color: red;
|
||||
font-weight: normal;
|
||||
`
|
||||
const P = styled('p')`
|
||||
color: rgba(0,0,0,0.7);
|
||||
`
|
||||
|
||||
const Highlighter = ({ family, errors, index = 0, words, milliseconds = 0, minutes = 0, wpm = 250, customStyles }) => {
|
||||
|
||||
const options = {
|
||||
overrides: {
|
||||
del: Striked,
|
||||
strong: Highlighted,
|
||||
p: {
|
||||
component: P,
|
||||
props: {
|
||||
style: {
|
||||
...customStyles,
|
||||
fontFamily: family
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const renderedWords = words.map((word, wordIndex) => {
|
||||
word = word.replace(/^ +| +$/gm, "")
|
||||
const newLines = /(\\n\\n|\\n)/g
|
||||
if (wordIndex === index){
|
||||
if (word.includes("\n")) {
|
||||
word = word.replace(newLines, '')
|
||||
return `**${word}**\n\n`
|
||||
} else {
|
||||
return `**${word}**`
|
||||
}
|
||||
}
|
||||
if (errors.includes(wordIndex) && index > wordIndex) {
|
||||
return `~~${word}~~`
|
||||
}
|
||||
return word
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMarkdown options={options} style={{ ...customStyles }}>{renderedWords.join(" ")}</StyledMarkdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const StyledMarkdown = styled(Markdown)`
|
||||
max-height: 64vh;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default Highlighter
|
156
components/Layout.js
Normal file
@ -0,0 +1,156 @@
|
||||
import { css, Global } from '@emotion/core'
|
||||
import Head from 'next/head'
|
||||
import Nav from './Nav'
|
||||
import Footer from './Footer'
|
||||
import Logo from './Logo'
|
||||
|
||||
const global = css`
|
||||
@font-face {
|
||||
font-family: 'Lexend';
|
||||
src: url('static/fonts/lexendgx.woff2') format('woff2'),
|
||||
url('static/fonts/lexendgx.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: white;
|
||||
min-height: 100%;
|
||||
font-family: "Lexend", Helvetica, Arial, sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
@media (max-width: 640px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
body {
|
||||
padding: 0 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: normal;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.7rem;
|
||||
line-height: 1.2;
|
||||
margin-top: 10vh;
|
||||
span {
|
||||
font-size: 30%;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
margin: 3rem 0 0.618rem;
|
||||
}
|
||||
h6 {
|
||||
color: #A9A9A9;
|
||||
font-variation-settings: 'LXND' 20;
|
||||
line-height: 1.4;
|
||||
}
|
||||
a {
|
||||
color: red;
|
||||
font-variation-settings: 'LXND' 80;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
text-decoration: none;
|
||||
font-size: 0.618rem !important;
|
||||
&:before {
|
||||
content: "⌁";
|
||||
margin: 0 0.192rem;
|
||||
font-size: 140%;
|
||||
}
|
||||
}
|
||||
p {
|
||||
max-width: 700px;
|
||||
color: rgba(0,0,0,0.76);
|
||||
line-height: 1.6;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
ul, ol {
|
||||
color: rgba(0,0,0,0.76);
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
line-height: 1.6;
|
||||
padding: 0 1rem 0;
|
||||
@media (max-width: 960px) {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
li {
|
||||
max-width: 100%;
|
||||
margin: 0.618rem 0 0.392rem;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.06);
|
||||
border-radius: 0.292rem;
|
||||
padding: 0.618rem;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
}
|
||||
code {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
border-radius: 0.192rem;
|
||||
padding: 0.392rem 0.618rem;
|
||||
background: white;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export default ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
styles={global}
|
||||
/>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="https://micahbrich.github.io/lexend/static/favicon/apple-touch-icon.png"/>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://micahbrich.github.io/lexend/static/favicon/favicon-32x32.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://micahbrich.github.io/lexend/static/favicon/favicon-16x16.png"/>
|
||||
<link rel="manifest" href="https://micahbrich.github.io/lexend/static/favicon/site.webmanifest"/>
|
||||
<link rel="mask-icon" href="https://micahbrich.github.io/lexend/static/favicon/safari-pinned-tab.svg" color="#5bbad5"/>
|
||||
<link rel="shortcut icon" href="https://micahbrich.github.io/lexend/static/favicon/favicon.ico"/>
|
||||
<meta name="msapplication-TileColor" content="#b91d47"/>
|
||||
<meta name="msapplication-config" content="https://micahbrich.github.io/lexend/static/favicon/browserconfig.xml"/>
|
||||
<meta name="theme-color" content="#ffffff"/>
|
||||
|
||||
|
||||
<title>Lexend — A Variable Font Designed for Reading</title>
|
||||
<meta name="title" content="Lexend — A Variable Font Designed for Reading"/>
|
||||
<meta name="description" content="Lexend is a variable font empirically shown to significantly improve reading-proficiency."/>
|
||||
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:url" content="https://micahbrich.github.io/lexend/"/>
|
||||
<meta property="og:title" content="Lexend — A Variable Font Designed for Reading"/>
|
||||
<meta property="og:description" content="Lexend is a variable font empirically shown to significantly improve reading-proficiency."/>
|
||||
<meta property="og:image" content="https://micahbrich.github.io/lexend/static/social/lexend.jpg"/>
|
||||
|
||||
<meta property="twitter:card" content="summary_large_image"/>
|
||||
<meta property="twitter:url" content="https://micahbrich.github.io/lexend/"/>
|
||||
<meta property="twitter:title" content="Lexend — A Variable Font Designed for Reading"/>
|
||||
<meta property="twitter:description" content="Lexend is a variable font empirically shown to significantly improve reading-proficiency."/>
|
||||
<meta property="twitter:image" content="https://micahbrich.github.io/lexend/static/social/lexend.jpg"/>
|
||||
|
||||
</Head>
|
||||
<Nav>
|
||||
<Logo/>
|
||||
</Nav>
|
||||
{ children }
|
||||
<Footer/>
|
||||
</>
|
||||
)
|
||||
}
|
62
components/Logo.js
Normal file
@ -0,0 +1,62 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { useTrail, animated } from "react-spring";
|
||||
import { useState } from "react";
|
||||
import useHover from '../lib/useHover'
|
||||
|
||||
const config = { mass: 0.2, tension: 100, friction: 10 }
|
||||
const items = "LEXEND".split("")
|
||||
const isE = letter => letter === "E"
|
||||
|
||||
export default () => {
|
||||
|
||||
const [hoverRef, hovering] = useHover()
|
||||
|
||||
const trail = useTrail(items.length, {
|
||||
config,
|
||||
opacity: hovering ? 1 : 0,
|
||||
x: hovering ? 0 : 20,
|
||||
width: hovering ? 25 : 0,
|
||||
fontVariationSettings: hovering ? "'LXND' 100" : "'LXND' 0",
|
||||
from: {
|
||||
opacity: 0,
|
||||
x: 20,
|
||||
width: 0,
|
||||
padding: 0,
|
||||
fontVariationSettings: "'LXND' 0"
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Logo ref={hoverRef}>
|
||||
{trail.map(({ x, width, opacity, ...rest }, index) => (
|
||||
<Span key={`logo-${items[index]}`} style={{
|
||||
x: isE(items[index]) ? x : null,
|
||||
width: isE(items[index]) ? width : null,
|
||||
opacity: isE(items[index]) ? opacity : null,
|
||||
...rest }}
|
||||
>
|
||||
{items[index]}
|
||||
</Span>
|
||||
))}
|
||||
|
||||
</Logo>
|
||||
);
|
||||
};
|
||||
|
||||
const Logo = styled("h1")`
|
||||
font-size: 1.4rem;
|
||||
cursor: default;
|
||||
background: red;
|
||||
display: inline-block;
|
||||
color: white;
|
||||
padding: 0.092em 0.392em;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const Span = styled(animated.span)`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
&:nth-of-type(4) { right: -1px; }
|
||||
`;
|
12
components/Nav.js
Normal file
@ -0,0 +1,12 @@
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
export default styled('nav')`
|
||||
border-bottom: 10px solid red;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
z-index: 99;
|
||||
`
|
22
components/Page.js
Normal file
@ -0,0 +1,22 @@
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
const Page = styled('div')`
|
||||
border: 1px solid rgba(0,0,0,0.24);
|
||||
padding: 2rem;
|
||||
margin: 2rem auto;
|
||||
font-size: 0.618rem;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255,255,255,0.8);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0px 2px 0.392rem rgba(0,0,0,0.05);
|
||||
border-radius: 3px;
|
||||
p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
/* height: 50vh; */
|
||||
}
|
||||
`
|
||||
export default Page
|
97
components/RangeSlider.js
Normal file
@ -0,0 +1,97 @@
|
||||
import styled from '@emotion/styled'
|
||||
import { jsx, css } from '@emotion/core'
|
||||
|
||||
|
||||
const RangeSlider = styled('input')`
|
||||
& {
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
margin: 0.618rem 0;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
box-shadow: 0.9px 0.9px 1.7px rgba(0, 34, 0, 0), 0px 0px 0.9px rgba(0, 60, 0, 0);
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 1px;
|
||||
border: 0px solid rgba(24, 213, 1, 0);
|
||||
}
|
||||
&::-webkit-slider-thumb {
|
||||
border: 1px solid rgba(0,0,0,0.4);
|
||||
height: 12px;
|
||||
width: 18px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
-webkit-appearance: none;
|
||||
margin-top: -5.5px;
|
||||
}
|
||||
&:focus::-webkit-slider-runnable-track {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
&::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
box-shadow: 0.9px 0.9px 1.7px rgba(0, 34, 0, 0), 0px 0px 0.9px rgba(0, 60, 0, 0);
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 1px;
|
||||
border: 0px solid rgba(24, 213, 1, 0);
|
||||
}
|
||||
&::-moz-range-thumb {
|
||||
border: 1px solid rgba(0,0,0,0.4);
|
||||
height: 12px;
|
||||
width: 18px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
}
|
||||
&::-ms-track {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
&::-ms-fill-lower {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border: 0px solid rgba(24, 213, 1, 0);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0.9px 0.9px 1.7px rgba(0, 34, 0, 0), 0px 0px 0.9px rgba(0, 60, 0, 0);
|
||||
}
|
||||
&::-ms-fill-upper {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border: 0px solid rgba(24, 213, 1, 0);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0.9px 0.9px 1.7px rgba(0, 34, 0, 0), 0px 0px 0.9px rgba(0, 60, 0, 0);
|
||||
}
|
||||
&::-ms-thumb {
|
||||
border: 1px solid rgba(0,0,0,0.4);
|
||||
height: 12px;
|
||||
width: 18px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
cursor: grab;
|
||||
&:active { cursor: grabbing; }
|
||||
height: 1px;
|
||||
}
|
||||
&:focus::-ms-fill-lower {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
&:focus::-ms-fill-upper {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
export default props => <RangeSlider type="range" {...props}/>
|
48
components/ReadingSimulator.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { useContext } from 'react'
|
||||
import TypeChoicesContext from '../lib/useTypeChoices'
|
||||
import ReadingSimulatorContext from '../lib/useReadingSimulator'
|
||||
import styled from '@emotion/styled'
|
||||
import Markdown from 'markdown-to-jsx'
|
||||
import Highlighter from './Highlighter'
|
||||
import ReadingStatistics from './ReadingStatistics'
|
||||
import ReadingSimulatorControls from './ReadingSimulatorControls'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
|
||||
|
||||
const Simulator = ({ disabled = [], ...props }) => {
|
||||
const { family, text, setText, setTypeFamily, axis, customStyles } = useContext(TypeChoicesContext)
|
||||
const { words, wpm, setWPM, started, setStarted, index, errors, timer } = useContext(ReadingSimulatorContext)
|
||||
const changeFamily = ({ target }) => setTypeFamily(target.value)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ReadingSimulatorControls
|
||||
disabled={disabled}
|
||||
wpm={wpm}
|
||||
setWPM={setWPM}
|
||||
started={started}
|
||||
text={text}
|
||||
setText={setText}
|
||||
on={() => setStarted(true)}
|
||||
off={() => setStarted(false)}
|
||||
family={props.family || family}
|
||||
changeFamily={changeFamily}
|
||||
/>
|
||||
<Highlighter family={props.family || family} words={words} index={index}
|
||||
errors={errors}
|
||||
customStyles={customStyles}/>
|
||||
<ReadingStatistics words={words} wpm={wpm} timer={timer} errors={errors} index={index}/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Container = styled('section')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default Simulator
|
81
components/ReadingSimulatorControls.js
Normal file
@ -0,0 +1,81 @@
|
||||
import texts from '../data/texts'
|
||||
import styled from '@emotion/styled'
|
||||
import { useContext } from 'react'
|
||||
import TypeChoicesContext from '../lib/useTypeChoices'
|
||||
import { Button } from '../components'
|
||||
import RangeSlider from './RangeSlider'
|
||||
|
||||
const ReadingSimulatorControls = ({ family, changeFamily, text, setText, disabled = [], wpm, setWPM, started, on, off }) => {
|
||||
const changeText = ({target}) => setText(target.value)
|
||||
return (
|
||||
<ControlBar>
|
||||
<WPMBar>
|
||||
<label>
|
||||
Avg WPM ({wpm})
|
||||
</label>
|
||||
<RangeSlider min="50" max="400" step="50" value={wpm}
|
||||
onChange={({ target }) => setWPM(target.value)}
|
||||
onMouseDown={off}
|
||||
onMouseUp={on}
|
||||
/>
|
||||
</WPMBar>
|
||||
<Select key="texts" disabled={disabled.includes('texts')} onChange={changeText} value={text}>
|
||||
{
|
||||
texts.map((_, i) => <option key={`text-${i}`} value={i}>Text #{i}</option>)
|
||||
}
|
||||
</Select>
|
||||
<Select key="family" onChange={changeFamily} disabled={disabled.includes('family')}
|
||||
value={family}>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Lexend">Lexend</option>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={() => started ? off() : on() }
|
||||
started={started}
|
||||
>
|
||||
{ started ? 'Stop' : 'Start'}
|
||||
</Button>
|
||||
</ControlBar>
|
||||
)
|
||||
}
|
||||
|
||||
const ControlBar = styled('nav')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.12);
|
||||
padding-bottom: 1em;
|
||||
margin-bottom: 3em;
|
||||
input {
|
||||
cursor: grab;
|
||||
width: 100%;
|
||||
}
|
||||
label {
|
||||
max-width: 20%;
|
||||
margin-left: 4px;
|
||||
}
|
||||
`
|
||||
|
||||
const Select = styled('select')`
|
||||
-webkit-appearance:none;
|
||||
height: 35px;
|
||||
padding: 0 0.818rem;
|
||||
font-size: 1em;
|
||||
background: white;
|
||||
border: 1px solid rgba(0,0,0,0.04);
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-color: white;
|
||||
}
|
||||
margin-left: 10px;
|
||||
outline: none;
|
||||
`
|
||||
const WPMBar = styled('div')`
|
||||
max-width: 30%;
|
||||
`
|
||||
|
||||
|
||||
export default ReadingSimulatorControls
|
110
components/ReadingStatistics.js
Normal file
@ -0,0 +1,110 @@
|
||||
import ReadingSimulatorContext from '../lib/useReadingSimulator'
|
||||
import { useContext } from 'react'
|
||||
import styled from '@emotion/styled'
|
||||
import ms from 'ms'
|
||||
import prettyMs from 'pretty-ms'
|
||||
|
||||
|
||||
|
||||
export const TotalNumberOfWords = () => {
|
||||
const { words } = useContext(ReadingSimulatorContext)
|
||||
return words.length
|
||||
}
|
||||
|
||||
export const TotalAmountOfTime = ({ as }) => {
|
||||
const { words, wpm } = useContext(ReadingSimulatorContext)
|
||||
if (as === "seconds") {
|
||||
return (words.length / wpm).toFixed(1) * 60
|
||||
}
|
||||
if (as === "decimal") {
|
||||
return (words.length / wpm).toFixed(1)
|
||||
}
|
||||
return prettyMs((words.length / wpm).toFixed(1) * 60000)
|
||||
}
|
||||
|
||||
export const TotalNumberOfErrors = () => {
|
||||
const { errors } = useContext(ReadingSimulatorContext)
|
||||
return errors.length
|
||||
}
|
||||
|
||||
export const TotalNumberOfCorrectWords = () => {
|
||||
const { words, errors } = useContext(ReadingSimulatorContext)
|
||||
return words.length - errors.length
|
||||
}
|
||||
|
||||
export const TotalWordsCorrectPerMinute = () => {
|
||||
return (TotalNumberOfCorrectWords() / TotalAmountOfTime({ as: "decimal" })).toFixed()
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const CorrectWordsPerMinute = (correctWords, time) => {
|
||||
const overTime = (correctWords / time) || 0
|
||||
// 60000 ms in a minute
|
||||
return Math.ceil(overTime * 60000)
|
||||
}
|
||||
|
||||
const ReadingStatistics = ({ words, wpm, timer, errors, index }) => {
|
||||
const minutes = (words.length / wpm).toFixed(2)
|
||||
const milliseconds = ms(`${minutes} min`)
|
||||
const currentErrors = errors.filter(e => e <= index).length || 0
|
||||
const cwpm = CorrectWordsPerMinute(index - currentErrors, timer)
|
||||
return (
|
||||
<Bar>
|
||||
<Stats>
|
||||
<strong>Time to read</strong>
|
||||
{prettyMs(timer)}
|
||||
</Stats>
|
||||
<Stats>
|
||||
<strong>Words read</strong>
|
||||
{index}
|
||||
</Stats>
|
||||
<Stats>
|
||||
<strong>Errors</strong>
|
||||
{currentErrors}
|
||||
</Stats>
|
||||
<Stats>
|
||||
<strong>Words Correct per Minute</strong>
|
||||
{cwpm} WCPM
|
||||
</Stats>
|
||||
</Bar>
|
||||
)
|
||||
}
|
||||
|
||||
const Bar = styled('div')`
|
||||
display: flex;
|
||||
width: 105%;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid rgba(0,0,0,0.48);
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
left: -2.5%;
|
||||
`
|
||||
|
||||
const Stats = styled('div')`
|
||||
margin: 0 0 0;
|
||||
text-align: right;
|
||||
padding: 1rem 1rem 0;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
font-size: 1.2rem;
|
||||
color: rgba(0,0,0,1);
|
||||
strong {
|
||||
font-size: 40%;
|
||||
display: block;
|
||||
margin-bottom: 0.192rem;
|
||||
font-variation-settings: 'LXND' 30;
|
||||
font-weight: normal;
|
||||
text-transform: uppercase;
|
||||
margin-right: -0.392rem;
|
||||
}
|
||||
`
|
||||
|
||||
export default ReadingStatistics
|
152
components/Tester.js
Normal file
@ -0,0 +1,152 @@
|
||||
import { useContext } from 'react'
|
||||
import useTypeChoices from '../lib/useTypeChoices'
|
||||
import styled from '@emotion/styled'
|
||||
import { useState, useEffect } from 'react'
|
||||
import useVariableFont from "react-variable-fonts"
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import texts from '../data/texts'
|
||||
import Page from './Page'
|
||||
import RangeSlider from './RangeSlider'
|
||||
|
||||
const initialSettings = {
|
||||
LXND: 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Tester = ({ weight, lxnd }) => {
|
||||
const { axis, setAxis, customStyles } = useContext(useTypeChoices)
|
||||
|
||||
switch (weight) {
|
||||
case "default":
|
||||
lxnd = 0
|
||||
break;
|
||||
case "regular":
|
||||
lxnd = 0
|
||||
break;
|
||||
case "deca":
|
||||
lxnd = 40
|
||||
break;
|
||||
case "kilo":
|
||||
lxnd = 40
|
||||
break;
|
||||
case "mega":
|
||||
lxnd = 56
|
||||
break;
|
||||
case "giga":
|
||||
lxnd = 64
|
||||
break;
|
||||
case "tera":
|
||||
lxnd = 72
|
||||
break;
|
||||
case "peta":
|
||||
lxnd = 80
|
||||
break;
|
||||
case "exa":
|
||||
lxnd = 100
|
||||
break;
|
||||
default:
|
||||
lxnd = axis.LXND
|
||||
break
|
||||
}
|
||||
|
||||
const style = {
|
||||
fontVariationSettings: `'LXND' ${lxnd}`
|
||||
}
|
||||
|
||||
const exampleCSS = `h1, p { font-variation-settings: 'LXND' ${axis.LXND}; }`
|
||||
return (
|
||||
<Container>
|
||||
{weight && <Weight style={{...style}}>{weight}</Weight>}
|
||||
<Title style={{ ...style }}>LEXEND</Title>
|
||||
|
||||
{
|
||||
!weight &&
|
||||
<Controls>
|
||||
<p style={{ ...style }}></p>
|
||||
<label htmlFor="lxnd-axis">
|
||||
Variable Width + Bounding Box
|
||||
</label>
|
||||
<RangeSlider
|
||||
id="lxnd-axis"
|
||||
type="range" min="0" max="100" value={axis.LXND}
|
||||
onChange={({ target }) => setAxis({ LXND: target.value })}
|
||||
/>
|
||||
<label htmlFor="lxnd-axis">
|
||||
Variable Font Settings for CSS
|
||||
</label>
|
||||
<pre>
|
||||
<code>{exampleCSS}</code>
|
||||
</pre>
|
||||
</Controls>
|
||||
}
|
||||
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = styled('section')`
|
||||
padding: 1.618rem 1.618rem;
|
||||
`
|
||||
const Weight = styled('span')`
|
||||
text-transform: uppercase;
|
||||
font-size: 80%;
|
||||
letter-spacing: 2px;
|
||||
`
|
||||
|
||||
const Controls = styled('nav')`
|
||||
max-width: 800px;
|
||||
padding: 1rem 0.618rem;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.618rem;
|
||||
@media (max-width: 960px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
input {
|
||||
cursor: grab;
|
||||
width: 100%;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.718rem;
|
||||
letter-spacing: 2px;
|
||||
margin: 2.618rem 0 0.618rem;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const H1 = styled('h1')`
|
||||
font-size: 5.4vw !important;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
span {
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
border: 1px dashed rgba(0,0,0, 0.12);
|
||||
}
|
||||
& + p {
|
||||
margin-left: 0.392rem;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
font-size: 12vw !important;
|
||||
}
|
||||
`
|
||||
|
||||
const Title = ({ children, ...props }) => (
|
||||
<H1>
|
||||
{
|
||||
children.split("")
|
||||
.map((child, i) => <span key={`${child}-${i}`} {...props}>{child}</span>)
|
||||
}
|
||||
</H1>
|
||||
)
|
||||
|
||||
export default Tester
|
145
components/Text.js
Normal file
@ -0,0 +1,145 @@
|
||||
import Highlighter from './Highlighter'
|
||||
import Timer from './Timer'
|
||||
import useInterval from '../lib/useInterval'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ms from 'ms'
|
||||
import prettyMs from 'pretty-ms'
|
||||
|
||||
function sample(array,size) {
|
||||
const results = [],
|
||||
sampled = {};
|
||||
while(results.length<size && results.length<array.length) {
|
||||
const index = Math.trunc(Math.random() * array.length);
|
||||
if(!sampled[index]) {
|
||||
results.push(index);
|
||||
sampled[index] = true;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const CorrectWordsPerMinute = (correctWords = 12, timeItTook = '12s') => {
|
||||
const timeInMS = ms(timeItTook)
|
||||
const overTime = (correctWords / timeInMS) || 0
|
||||
// 60000 ms in a minute
|
||||
return Math.ceil(overTime * 60000)
|
||||
}
|
||||
|
||||
const useWPMSimulator = (words) => {
|
||||
const toggle = () => {
|
||||
setStarted(!started)
|
||||
setIndex(0)
|
||||
setTimer(0)
|
||||
}
|
||||
|
||||
const [started, setStarted] = useState(false)
|
||||
const [errors, setErrors] = useState(sample(words, Math.floor(Math.random() * words.length/2)))
|
||||
|
||||
const [wpm, setWPM] = useState(250)
|
||||
const [index, setIndex] = useState(0)
|
||||
const [timer, setTimer] = useState(0)
|
||||
|
||||
const [stats, setStats] = useState({
|
||||
minutes: 0,
|
||||
milliseconds: 0
|
||||
})
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const wpms = ms(`${1/wpm}m`)
|
||||
if (!started) {
|
||||
setIndex(0)
|
||||
setTimer(0)
|
||||
}
|
||||
if (index <= words.length - 2) {
|
||||
const wordCounter = setInterval(() => {
|
||||
setIndex(index + 1)
|
||||
setTimer(index*wpms)
|
||||
}, wpms)
|
||||
return () => clearInterval(wordCounter)
|
||||
}
|
||||
}, [index])
|
||||
|
||||
useEffect(() => {
|
||||
const minutes = (words.length / wpm).toFixed(2)
|
||||
const milliseconds = ms(`${minutes} min`)
|
||||
setStats({ minutes, milliseconds })
|
||||
setIndex(0)
|
||||
setTimer(0)
|
||||
return () => {}
|
||||
}, [wpm])
|
||||
|
||||
return {
|
||||
wpm,
|
||||
setWPM,
|
||||
stats,
|
||||
setStats,
|
||||
started,
|
||||
setStarted,
|
||||
toggle,
|
||||
errors,
|
||||
index,
|
||||
setIndex,
|
||||
timer,
|
||||
setTimer
|
||||
}
|
||||
}
|
||||
|
||||
const Text = ({ content }) => {
|
||||
const words = content.match(/\b(\w+\W+)/g)
|
||||
|
||||
const {
|
||||
wpm,
|
||||
setWPM,
|
||||
stats,
|
||||
setStats,
|
||||
started,
|
||||
setStarted,
|
||||
toggle,
|
||||
errors,
|
||||
index,
|
||||
setIndex,
|
||||
timer,
|
||||
setTimer
|
||||
} = useWPMSimulator(words)
|
||||
|
||||
const currentErrors = errors.filter(e => e <= index).length || 0
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Words per minute ({wpm})</label><br/>
|
||||
<input
|
||||
type="range" min="50" max="400" step="50" value={wpm}
|
||||
onChange={({ target }) => setWPM(target.value)}
|
||||
onMouseDown={() => setStarted(false)}
|
||||
onMouseUp={() => setStarted(true)}
|
||||
/>
|
||||
<button onClick={toggle}>{ started ? 'Stop' : 'Start'}</button>
|
||||
</div>
|
||||
|
||||
<Highlighter errors={errors} index={index} setIndex={setIndex} wpm={wpm} text={content} words={words} {...stats}/>
|
||||
|
||||
<h3>Ideal</h3>
|
||||
<p>
|
||||
<strong>Time to read:</strong> {prettyMs(stats.milliseconds)}
|
||||
</p>
|
||||
<p>
|
||||
{index} <strong>words read </strong>
|
||||
- 0 <strong>errors </strong>
|
||||
= {CorrectWordsPerMinute(index, prettyMs(timer))} <strong>Words correct per minute</strong>
|
||||
</p>
|
||||
|
||||
<h3>Actual</h3>
|
||||
<p>
|
||||
<strong>Time to read:</strong> {prettyMs(timer)}
|
||||
</p>
|
||||
<p>
|
||||
{index} <strong>words read </strong>
|
||||
- {currentErrors} <strong>errors </strong>
|
||||
= {CorrectWordsPerMinute(index - currentErrors, prettyMs(timer))} <strong>Words correct per minute</strong>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Text
|
10
components/Timer.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import prettyMs from 'pretty-ms'
|
||||
|
||||
const Timer = ({ timer }) => {
|
||||
return (
|
||||
<p><strong>Time to read:</strong> {prettyMs(timer)}</p>
|
||||
)
|
||||
}
|
||||
|
||||
export default Timer
|
189
components/index.js
Normal file
@ -0,0 +1,189 @@
|
||||
import styled from '@emotion/styled'
|
||||
import { css } from '@emotion/core'
|
||||
|
||||
export const Flex = styled('section')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 4rem auto;
|
||||
align-items: flex-start;
|
||||
`
|
||||
|
||||
export const Right = styled('main')`
|
||||
margin: 1rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: ${ props => props.width || '45%'};
|
||||
width: ${ props => props.width || 'auto'};
|
||||
min-width: 400px;
|
||||
${ props => props.sticky && css`
|
||||
position: sticky;
|
||||
top: ${ props.top || '1rem'};
|
||||
`}
|
||||
@media (max-width: 960px) {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
position: static;
|
||||
}
|
||||
`
|
||||
|
||||
export const Left = styled('main')`
|
||||
margin: 2.618rem auto;
|
||||
padding: 0 1rem;
|
||||
max-width: ${ props => props.width || '45%'};
|
||||
width: ${ props => props.width || 'auto'};
|
||||
height: ${ props => props.height || 'auto'};
|
||||
${ props => props.sticky && css`
|
||||
position: sticky;
|
||||
top: ${ props.top || '1rem'};
|
||||
`}
|
||||
@media (max-width: 960px) {
|
||||
max-width: 100%;
|
||||
position: static;
|
||||
}
|
||||
`
|
||||
|
||||
export const Button = styled("button")`
|
||||
-webkit-appearance:none;
|
||||
font-family: "Lexend", Helvetica, Arial, sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 0.618em 1em;
|
||||
transition: all 300ms ease-in;
|
||||
outline: none;
|
||||
min-width: 70px;
|
||||
background: ${props => props.started ? 'white' : 'red'};
|
||||
color: ${props => props.started ? 'black' : 'white'};
|
||||
@media (max-width: 1020px) {
|
||||
width: 100%;
|
||||
margin-top: 0.392rem;
|
||||
}
|
||||
`
|
||||
|
||||
export const Intro = styled('section')`
|
||||
padding: 1rem;
|
||||
h1 {
|
||||
font-size: 7.2vw;
|
||||
line-height: 1.1;
|
||||
margin: 14vh 3.618vw 3.618rem;
|
||||
font-weight: normal;
|
||||
strong {
|
||||
color: red;
|
||||
font-weight: normal;
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
width: 100%;
|
||||
font-size: 12vw;
|
||||
line-height: 1.2;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: normal;
|
||||
max-width: 60%;
|
||||
margin: 1rem auto 0.618rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.4rem;
|
||||
line-height: 1.4;
|
||||
@media (max-width: 960px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
h6 {
|
||||
max-width: 30%;
|
||||
margin: 0 auto;
|
||||
font-variation-settings: 'LXND' 20;
|
||||
@media (max-width: 960px) {
|
||||
max-width: 88%;
|
||||
}
|
||||
a {
|
||||
font-variation-settings: 'LXND' 80;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
text-decoration: none;
|
||||
&:before {
|
||||
content: "⌁";
|
||||
margin: 0 0.192rem;
|
||||
font-size: 140%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-variation-settings: 'LXND' 10;
|
||||
color: rgba(0,0,0,0.76);
|
||||
line-height: 1.6;
|
||||
font-size: 1.2rem;
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
max-width: 500px;
|
||||
}
|
||||
a {
|
||||
color: red;
|
||||
}
|
||||
ul, ol {
|
||||
max-width: 720px;
|
||||
margin: 1.618rem auto;
|
||||
}
|
||||
blockquote {
|
||||
font-weight: normal;
|
||||
display: block;
|
||||
max-width: 760px;
|
||||
background: white;
|
||||
padding: 0.618rem 2.618rem;
|
||||
margin: 4rem auto 1rem;
|
||||
position: relative;
|
||||
left: -2rem;
|
||||
border: 0.618rem solid red;
|
||||
@media (max-width: 960px) {
|
||||
max-width: 100%;
|
||||
left: 0;
|
||||
left: 0;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.618;
|
||||
}
|
||||
&:before {
|
||||
font-weight: normal;
|
||||
content:"“";
|
||||
font-size: 9.6rem;
|
||||
position: absolute;
|
||||
top: -4.618rem;
|
||||
left: 1.618rem;
|
||||
color: black;
|
||||
}
|
||||
&:after {
|
||||
font-weight: normal;
|
||||
content:"”";
|
||||
font-size: 9.6rem;
|
||||
position: absolute;
|
||||
bottom: -8.618rem;
|
||||
right: 1.618rem;
|
||||
color: black;
|
||||
}
|
||||
img {
|
||||
max-width: 400px;
|
||||
position: absolute;
|
||||
right: -40%;
|
||||
bottom: -4%;
|
||||
filter: drop-shadow(0.618rem 0.618rem 0.392rem rgba(0,0,0,0.12));
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
max-width: 220px;
|
||||
position: static;
|
||||
margin-top: -8rem;
|
||||
margin-left: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
116
data/TNRvsLXND.js
Normal file
@ -0,0 +1,116 @@
|
||||
module.exports = [
|
||||
{
|
||||
"Student": "#1",
|
||||
"Times New Roman": 93,
|
||||
"Lexend": 103,
|
||||
"Percent Difference": 10.8
|
||||
},
|
||||
{
|
||||
"Student": "#2",
|
||||
"Times New Roman": 93,
|
||||
"Lexend": 104,
|
||||
"Percent Difference": 11.8
|
||||
},
|
||||
{
|
||||
"Student": "#3",
|
||||
"Times New Roman": 149,
|
||||
"Lexend": 175,
|
||||
"Percent Difference": 17.4
|
||||
},
|
||||
{
|
||||
"Student": "#4",
|
||||
"Times New Roman": 90,
|
||||
"Lexend": 107,
|
||||
"Percent Difference": 18.9
|
||||
},
|
||||
{
|
||||
"Student": "#5",
|
||||
"Times New Roman": 154,
|
||||
"Lexend": 165,
|
||||
"Percent Difference": 7.1
|
||||
},
|
||||
{
|
||||
"Student": "#6",
|
||||
"Times New Roman": 160,
|
||||
"Lexend": 154,
|
||||
"Percent Difference": -3.8
|
||||
},
|
||||
{
|
||||
"Student": "#7",
|
||||
"Times New Roman": 78,
|
||||
"Lexend": 92,
|
||||
"Percent Difference": 17.9
|
||||
},
|
||||
{
|
||||
"Student": "#8",
|
||||
"Times New Roman": 96,
|
||||
"Lexend": 125,
|
||||
"Percent Difference": 30.2
|
||||
},
|
||||
{
|
||||
"Student": "#9",
|
||||
"Times New Roman": 159,
|
||||
"Lexend": 155,
|
||||
"Percent Difference": -2.5
|
||||
},
|
||||
{
|
||||
"Student": "#10",
|
||||
"Times New Roman": 91,
|
||||
"Lexend": 118,
|
||||
"Percent Difference": 29.7
|
||||
},
|
||||
{
|
||||
"Student": "#11",
|
||||
"Times New Roman": 89,
|
||||
"Lexend": 111,
|
||||
"Percent Difference": 24.7
|
||||
},
|
||||
{
|
||||
"Student": "#12",
|
||||
"Times New Roman": 178,
|
||||
"Lexend": 199,
|
||||
"Percent Difference": 11.8
|
||||
},
|
||||
{
|
||||
"Student": "#13",
|
||||
"Times New Roman": 90,
|
||||
"Lexend": 125,
|
||||
"Percent Difference": 38.9
|
||||
},
|
||||
{
|
||||
"Student": "#14",
|
||||
"Times New Roman": 121,
|
||||
"Lexend": 149,
|
||||
"Percent Difference": 23.1
|
||||
},
|
||||
{
|
||||
"Student": "#15",
|
||||
"Times New Roman": 71,
|
||||
"Lexend": 89,
|
||||
"Percent Difference": 25.4
|
||||
},
|
||||
{
|
||||
"Student": "#16",
|
||||
"Times New Roman": 154,
|
||||
"Lexend": 165,
|
||||
"Percent Difference": 7.1
|
||||
},
|
||||
{
|
||||
"Student": "#17",
|
||||
"Times New Roman": 135,
|
||||
"Lexend": 166,
|
||||
"Percent Difference": 23.0
|
||||
},
|
||||
{
|
||||
"Student": "#18",
|
||||
"Times New Roman": 36,
|
||||
"Lexend": 53,
|
||||
"Percent Difference": 47.2
|
||||
},
|
||||
{
|
||||
"Student": "#19",
|
||||
"Times New Roman": 53,
|
||||
"Lexend": 73,
|
||||
"Percent Difference": 37.7
|
||||
}
|
||||
]
|
59
data/texts.js
Normal file
@ -0,0 +1,59 @@
|
||||
export default [
|
||||
`
|
||||
My name is Commander Smith and the spaceship that I command has been exploring the planet known as Earth for almost a year. We have discovered that Earth is very different from our planet. Our next job was to explore the states of Texas, New Mexico, Arizona, and Nevada. These states make up the Southwest region.
|
||||
|
||||
First, we had to backtrack a little and fly east to Texas. This is a huge state—the map showed that it was more than seven hundred miles wide. Later we found out that Texas is the second largest state in size, after Alaska. It is also the second largest state in population, after California. We certainly saw a lot of really big things in Texas—big cities, big oil fields, and big ranches. In fact, we found out that there is a ranch in Texas that is bigger than the whole state of Rhode Island! Texas raises more cattle than any other state. The cowboy, a person who takes care of the cattle, is a symbol of Texas. In the late 1800s, Texas cowboys drove the cattle hundreds of miles to market in the northern states.
|
||||
|
||||
It was hard, dangerous, adventurous work, so many people thought of cowboys as heroes. Over time, the cattle ranches changed so that there was less need for cowboys. Today, there are not as many cowboys in
|
||||
|
||||
Texas. There are, however, many Texans who still dress like cowboys. They wear cowboy boots and a kind of tall cowboy hat they call a ten- gallon-hat because it looks as if it could hold that much water. Another symbol of Texas is the oil well. Texas produces more oil than any other state. Oil was first discovered near the city of Houston in the early 1900s.
|
||||
|
||||
Today, Houston is the largest city in Texas. The American space program has one of its largest workplaces there. One man told us that Houston was the first word spoken on the moon. He explained that when an American became the first human to land on the moon on July 20,1969. The first thing he did was radio back to Houston. The United States was the first manned mission to land on the Moon. There have been six manned U.S. landings and numerous unmanned landings. To date, the United States is the only country to have successfully conducted manned missions to the Moon, with the last one in December 1972.`,
|
||||
|
||||
|
||||
`
|
||||
In our exploration of Texas and the other Southwestern states, we heard many people speaking Spanish, the same language we had heard spoken in parts of Florida. When we studied the history of the Southwest, we learned the reason for this. The Southwest region was not always part of the United States. Instead, it was part of Mexico, the country just to the south of the United States. There, people speak Spanish. In the 1840s, the United States and Mexico went to war over large areas of land in the Southwest. The United States won the war and gained the land that became parts of Texas, New Mexico, Arizona, Nevada, Utah, Colorado, and California.
|
||||
|
||||
Mexican culture still has a strong influence on this region. Many cities here have Spanish names. Today, the region is home to millions of Mexican Americans, many of whom speak both Spanish and English. Spanish is the second most spoken language in the world. Almost 400 million people speak Spanish. English is the third most spoken language in the world, with 335 million people speaking English. Many people in the Southwest enjoy being able to share in the blending of the wonderful food and culture of the region.
|
||||
Much of the Southwest is desert-dry, sandy land with few trees. The desert begins in western Texas and covers much of New Mexico, Arizona, and Nevada. Very little rain falls in the desert.
|
||||
|
||||
In the summer, it gets hotter than any other part of the United States. Temperatures of 115 or 120 degrees are not unusual. In the summer when the temperature is the hottest most people try to stay inside out of the heat. It gets so hot that people are told not to take their pets outside for walks. This is so pets will not get sick from getting too hot or get burns on their paws. The desert is beautiful, but the climate is not like most of the rest of the country.
|
||||
|
||||
We landed our ship in southern Arizona and went exploring. The desert was very different from most of the other landscapes we had seen. We almost felt as if we were on another planet. Instead of trees, we saw cactus. Some of the cactus were short and round. Others were very tall and had thick "arms" sticking upward from their trunks.`,
|
||||
|
||||
|
||||
`
|
||||
Suddenly we saw something strange-a little bird with long tail feathers and spiky feathers on its head, running quickly across the sand.
|
||||
|
||||
We had never seen a bird run before. Commander Smith explained, "That's a roadrunner. He can fly, but he would rather run." Shewent on to say, "You know, a lot of people think there is no life in the desert, but that is not true. Look- over there is a jackrabbit." Wesawa large brown rabbit hopping out of a bush. Then I said, "Wow! I have found a little lobster." When we looked down, I saw something that did look like a tiny lobster, except it had a long tail that curled back over its body. Commander Smith yelled, "Do not touch that! It is a scorpion. It has a poisonous stinger in its tail, and if it stings you, you will get very sick. "When I heard that it wasnot safe, I jumped back a couple of feet. Fortunately, everyone was okay and so we could continue exploring.
|
||||
|
||||
So, after wehad explored the desert for a little while, I decided that weshould get back inside our ship and fly to northern Arizona. I really wanted to see the Grand Canyon. The Grand Canyon is one of the great natural wonders of the United States.
|
||||
|
||||
It looks like a big hole has been carved out of the Colorado River. It is 277 miles long, over a mile deep, and up to eighteen miles across. Even though it is a canyon it also has many caves which people lived in many years ago.
|
||||
If you look out over the edge of the Grand Canyon, you can see the river glistening far, far down at the bottom. Thewalls of the canyon are all different colors - red, brown, white, and yellow. Thecolors come from the layers of different kinds of rock. Like most visitors to the canyon, we just stood there for a long time, looking at it in awe. People on Earth were smart to call this canyon the Grand Canyon because it is very Grand!
|
||||
|
||||
After flying over Nevada, we headed west again, toward the Pacific Ocean. The part of the United States that borders the Pacific is known as the West Coast.`,
|
||||
|
||||
|
||||
`
|
||||
There are three states on the West Coast - CaIifornia, Oregon, and Washington. California is by far the biggest of the three, covering two- thirds of the coast. It also has more people than any other state in the country.
|
||||
|
||||
We decided to begin our exploration of theWest Coast in California. We first visited the city of LosAngeles, the second largest city in the United States. Flying over the city, we were amazed by its size. It spread out as far as the eye couId see in every direction. Welearned that large roads called freeways tie this vast city together. LosAngeles is so spread out that people sometimes spend hours every day in their cars, driving from home to work and then back again to home. Because almost everyone has a car, there are too many cars on the roads, and traffic gets jammed. Americans call the times that they drive to work in the morning and back home at night rush hour.
|
||||
The vast city of LosAngeles is home to people of many different backgrounds. As you wander around you hear not only English and Spanish, but also other languages. In fact, there are more than 200 languages spoken in the city of Los Angeles. One of the things I wi11 remember about planet Earth is all the beautiful sounds of all the different languages. Even though languages can sound different, laughter sounds the same everywhere we travel on Earth.
|
||||
|
||||
Los Angeles is famous for its beautifuI sandy beaches. On summer days, the beaches are packed with people who have come there to enjoy themselves. While exploring, we saw some people surfing. The surfers paddled out into the water with their long surf boards. The city of Lo sAngeles is also famous for being the world capital of the movie business. Thousands of movies have been made here.
|
||||
|
||||
Los Angeles is in the southern part of California. Thesouthern part of California, like the Southwest does not receive a large amount of rain. Not getting enough rain can be very serious. People there have developed new technology to solve water shortage problems. No matter where we travel, people on Earth on very smart and find ways to solve their problems.`,
|
||||
|
||||
|
||||
`
|
||||
Flying north to explore the rest of the state, we saw some very different landscapes. To the north, the California coast becomes rocky instead of sandy. Tall cliffs plunge very far down to the ocean, and huge waves explode against the rocks. It is very beautiful.
|
||||
|
||||
Flying inland from the coast, we crossed a range of mountains. Then we came to a long valley that stretches all the way down the center of the state. When we flew low over the valley, we saw that it was covered with farms.
|
||||
|
||||
Later, we learned that this is one of the most important food growing regions of the United States. This region called The Central Valley is 40 to 60 miles wide and stretches 450 miles from north to south. It provides more than half of the fruits, vegetables and nuts grown in the United States.
|
||||
|
||||
Flying farther east, we came to a range of tall mountains. According to the map, the tallest mountain in this range is also the tallest mountain in the United States outside of Alaska. As beautiful as the mountains were, we were eager to see California's other famous city, San Francisco. So, we turned our ship back toward the coast. San Francisco is very different from Los Angeles. San Francisco is full of new skyscrapers as well as old houses. Its parks are lush and green, displaying interesting trees and flowers collected from all over Earth. It is full of all the colors of the rainbow.
|
||||
|
||||
The city sits beside a beautiful blue bay. Spanning across this area of water is the famous Golden Gate Bridge. The bridge's two towers are seventy stories tall, and they are connected to the bridge by great swooping cables. Even though the name of the Golden Gate Bridge makes some people think that the bridge might be painted gold, it is not. Just think, that would be a lot of gold paint since the towers are 70 stories tall! Well, even though it has been fun to be in San Francisco and many other places on Earth it is time to go back home. We will always remember traveling to Earth and meeting so many wonderful people and visiting so many beautiful places! Good-bye for now!`
|
||||
]
|
19
lib/array.js
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
export function sample(array,size) {
|
||||
const results = [],
|
||||
sampled = {};
|
||||
|
||||
while(results.length < size && results.length < array.length) {
|
||||
const index = Math.trunc(Math.random() * array.length);
|
||||
if(!sampled[index] && array[index].length >= 6) {
|
||||
results.push(index);
|
||||
sampled[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function randomNumber (array) {
|
||||
return Math.floor(Math.random() * array.length/4)
|
||||
}
|
28
lib/useHover.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
||||
export default function useHover() {
|
||||
const [value, setValue] = useState(false);
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
const handleMouseOver = () => setValue(true);
|
||||
const handleMouseOut = () => setValue(false);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const node = ref.current;
|
||||
if (node) {
|
||||
node.addEventListener('mouseover', handleMouseOver);
|
||||
node.addEventListener('mouseout', handleMouseOut);
|
||||
|
||||
return () => {
|
||||
node.removeEventListener('mouseover', handleMouseOver);
|
||||
node.removeEventListener('mouseout', handleMouseOut);
|
||||
};
|
||||
}
|
||||
},
|
||||
[ref.current] // Recall only if ref changes
|
||||
);
|
||||
|
||||
return [ref, value];
|
||||
}
|
23
lib/useInterval.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
function useInterval(callback, delay) {
|
||||
const savedCallback = useRef();
|
||||
|
||||
// Remember the latest callback.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
export default useInterval
|
39
lib/useMediaQuery.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function useMedia(queries, values, defaultValue) {
|
||||
const client = typeof window === 'object'
|
||||
|
||||
if (!client) { return defaultValue }
|
||||
|
||||
// Array containing a media query list for each query
|
||||
const mediaQueryLists = queries.map(q => window.matchMedia(q));
|
||||
|
||||
// Function that gets value based on matching media query
|
||||
const getValue = () => {
|
||||
// Get index of first media query that matches
|
||||
const index = mediaQueryLists.findIndex(mql => mql.matches);
|
||||
// Return related value or defaultValue if none
|
||||
return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
|
||||
};
|
||||
|
||||
// State and setter for matched value
|
||||
const [value, setValue] = useState(getValue);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Event listener callback
|
||||
// Note: By defining getValue outside of useEffect we ensure that it has ...
|
||||
// ... current values of hook args (as this hook callback is created once on mount).
|
||||
const handler = () => setValue(getValue);
|
||||
// Set a listener for each media query with above handler as callback.
|
||||
mediaQueryLists.forEach(mql => mql.addListener(handler));
|
||||
// Remove listeners on cleanup
|
||||
return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
|
||||
},
|
||||
[] // Empty array ensures effect is only run on mount and unmount
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default useMedia
|
28
lib/useOnScreen.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function useOnScreen(ref, rootMargin = '0px') {
|
||||
// State and setter for storing whether element is visible
|
||||
const [isIntersecting, setIntersecting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
// Update our state when observer callback fires
|
||||
setIntersecting(entry.isIntersecting);
|
||||
},
|
||||
{
|
||||
rootMargin
|
||||
}
|
||||
);
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
return () => {
|
||||
observer.unobserve(ref.current);
|
||||
};
|
||||
}, []); // Empty array ensures that effect is only run on mount and unmount
|
||||
|
||||
return isIntersecting;
|
||||
}
|
||||
|
||||
export default useOnScreen
|
69
lib/useReadingSimulator.js
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState, useEffect, createContext, useContext } from 'react'
|
||||
import { sample, randomNumber } from '../lib/array.js'
|
||||
import useVariableFont from "react-variable-fonts"
|
||||
import texts from '../data/texts'
|
||||
import ms from 'ms'
|
||||
import TypeChoices from './useTypeChoices'
|
||||
|
||||
const ReadingSimulatorContext = createContext()
|
||||
|
||||
|
||||
export const ReadingSimulatorProvider = ({ children }) => {
|
||||
|
||||
const { content } = useContext(TypeChoices)
|
||||
const words = content.match(/\b(\w+\W+)/g)
|
||||
|
||||
const [wpm, setWPM] = useState(300)
|
||||
const [index, setIndex] = useState(0)
|
||||
const [started, setStarted] = useState(false)
|
||||
const [timer, setTimer] = useState(0)
|
||||
const [errors, setErrors] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const numberOfErrors = randomNumber(words) + 7
|
||||
let randomArrayOfIndexes = sample(
|
||||
words,
|
||||
numberOfErrors
|
||||
)
|
||||
randomArrayOfIndexes = randomArrayOfIndexes.filter(i => i !== 0)
|
||||
setErrors(randomArrayOfIndexes)
|
||||
}, [content])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const wpms = ms(`${1/wpm}m`)
|
||||
if (started) {
|
||||
if (index >= words.length - 1) {
|
||||
setStarted(false)
|
||||
}
|
||||
if (index <= words.length - 1) {
|
||||
const wordCounter = setInterval(() => {
|
||||
setIndex(index + 1)
|
||||
setTimer(index*wpms)
|
||||
}, wpms)
|
||||
return () => clearInterval(wordCounter)
|
||||
}
|
||||
}
|
||||
}, [started, index])
|
||||
|
||||
useEffect(() => {
|
||||
setIndex(0)
|
||||
setTimer(0)
|
||||
return () => {}
|
||||
}, [wpm])
|
||||
|
||||
|
||||
return <ReadingSimulatorContext.Provider value={{
|
||||
words,
|
||||
wpm,
|
||||
setWPM,
|
||||
index,
|
||||
setIndex,
|
||||
errors,
|
||||
timer,
|
||||
started,
|
||||
setStarted
|
||||
}}>{children}</ReadingSimulatorContext.Provider>
|
||||
}
|
||||
|
||||
export default ReadingSimulatorContext
|
37
lib/useTypeChoices.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { useState, useEffect, createContext, useContext } from 'react'
|
||||
import useVariableFont from "react-variable-fonts"
|
||||
import texts from '../data/texts'
|
||||
const TypeChoicesContext = createContext()
|
||||
|
||||
export const TypeChoicesProvider = ({ children }) => {
|
||||
|
||||
let content
|
||||
|
||||
const [text, setText] = useState(0)
|
||||
const [family, setTypeFamily] = useState('Lexend')
|
||||
const [axis, setAxis] = useState({ LXND: 0 })
|
||||
const [customStyles, updateStyles] = useVariableFont("Lexend", {})
|
||||
|
||||
useEffect(() => {
|
||||
updateStyles(axis)
|
||||
return () => {}
|
||||
}, [axis])
|
||||
|
||||
|
||||
return <TypeChoicesContext.Provider value={{
|
||||
content: texts[text],
|
||||
text,
|
||||
setText,
|
||||
axis,
|
||||
setAxis,
|
||||
customStyles,
|
||||
updateStyles,
|
||||
family,
|
||||
setTypeFamily
|
||||
}}>{children}</TypeChoicesContext.Provider>
|
||||
}
|
||||
|
||||
export const TypeChoices = TypeChoicesContext.Consumer
|
||||
|
||||
|
||||
export default TypeChoicesContext
|
9
next.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
const withMDX = require('@zeit/next-mdx')({
|
||||
extension: /\.mdx?$/
|
||||
})
|
||||
|
||||
|
||||
module.exports = withMDX({
|
||||
pageExtensions: ['js', 'jsx', 'mdx'],
|
||||
assetPrefix: process.env.NODE_ENV === 'production' ? '/lexend' : ''
|
||||
})
|
4
now.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [{ "src": "package.json", "use": "@now/static-build" }]
|
||||
}
|
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "lexend",
|
||||
"version": "1.0.0",
|
||||
"description": "a microsite demonstrating the awesome capabilities of Lexend",
|
||||
"main": "pages/index.js",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"now-build": "next build && next export -o dist && touch dist/.nojekyll && touch dist/_next/.nojekyll",
|
||||
"predeploy": "next build && next export -o dist && touch dist/.nojekyll",
|
||||
"deploy": "gh-pages -t -d dist",
|
||||
"start": "next start"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "micah rich",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/core": "^10.0.10",
|
||||
"@emotion/styled": "^10.0.11",
|
||||
"@mdx-js/loader": "^1.0.19",
|
||||
"@nivo/bar": "^0.58.0",
|
||||
"@zeit/next-mdx": "^1.2.0",
|
||||
"babel-plugin-emotion": "^10.0.9",
|
||||
"markdown-to-jsx": "^6.9.4",
|
||||
"ms": "^2.1.1",
|
||||
"next": "^8.1.0",
|
||||
"parse-ms": "^2.1.0",
|
||||
"pretty-ms": "^5.0.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-spring": "^8.0.20",
|
||||
"react-variable-fonts": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gh-pages": "^2.0.1"
|
||||
}
|
||||
}
|
218
pages/index.mdx
Normal file
@ -0,0 +1,218 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import useVariableFont from "react-variable-fonts"
|
||||
import { Flex, Left, Right, Intro } from '../components'
|
||||
import { TypeChoicesProvider } from '../lib/useTypeChoices'
|
||||
import { ReadingSimulatorProvider } from '../lib/useReadingSimulator'
|
||||
import AnimatedNumber from '../components/AnimatedNumber'
|
||||
import styled from '@emotion/styled'
|
||||
import texts from '../data/texts'
|
||||
import Page from '../components/Page'
|
||||
import Layout from '../components/Layout'
|
||||
import ReadingSimulator from '../components/ReadingSimulator'
|
||||
import { TotalNumberOfWords, TotalAmountOfTime, TotalNumberOfErrors, TotalNumberOfCorrectWords, TotalWordsCorrectPerMinute } from '../components/ReadingStatistics'
|
||||
import Text from '../components/Text'
|
||||
import Tester from '../components/Tester'
|
||||
import Chart from '../components/Chart'
|
||||
import ChartStats from '../components/ChartStats'
|
||||
import Form from '../components/EmailForm'
|
||||
import ArabicExample from '../components/ArabicExample'
|
||||
|
||||
|
||||
<Intro>
|
||||
|
||||
# Lexend is a variable font empirically shown to significantly improve reading-proficiency.
|
||||
|
||||
As prescription eyeglasses achieve proficiency for persons with short-sightedness, Lexend employs variable font technology to prescribe a personalized axis setting according to the Shaver-Troup Formulations.
|
||||
|
||||
> ![](https://micahbrich.github.io/lexend/static/images/noordzji-cover-cut.png) The word is the condition for reading. The word consists of both white and black forms in rhythm units. If the rhythm is weak; the word is poorly formed. If the rhythm is absent; there is no word. Reading is a spatial manner of intervals in both length and breadth.... Ignoring the white by pedagogy places civilization at risk. What is civilization if not the cultural community dependent on reading?
|
||||
|
||||
###### Excerpts from “The Stroke” by Gerrit Noordzij
|
||||
|
||||
# The US Department of Education reports that **only <AnimatedNumber key="us-dept-of-ed">36</AnimatedNumber>% of students are reading-proficient.** This would appear to confirm Noordzij’s hyperbolic warning.
|
||||
|
||||
###### [Source](https://www.nationsreportcard.gov/reading_math_2015/#reading?grade=4) • Retrieved 30 August 2017
|
||||
|
||||
|
||||
</Intro>
|
||||
|
||||
<Flex>
|
||||
<Left sticky>
|
||||
<Page>
|
||||
<ReadingSimulator family="Lexend" disabled={['family']} />
|
||||
</Page>
|
||||
</Left>
|
||||
|
||||
<Right>
|
||||
|
||||
## Reading-Proficiency is measured in fluency.
|
||||
|
||||
There is a consensus that reading fluency is one of the defining characteristics of good readers, and a lack of fluency is a common characteristic of poor readers.
|
||||
|
||||
Differences in reading fluency not only distinguish good readers from poor, but a lack of reading fluency is also a reliable predictor of reading comprehension problems. Once struggling readers learn sound-symbol relationships through intervention and become accurate decoders, their lack of fluency emerges as the next hurdle they face on their way to reading proficiency.
|
||||
|
||||
This lack of fluent reading is a problem for poor readers because they tend to read in a labored, disconnected fashion with a focus on decoding at the word level that makes comprehension of the text difficult, if not impossible.
|
||||
|
||||
|
||||
###### [Source](https://www.fcrr.org/publications/publicationspdffiles/hudson_lane_pullen_readingfluency_2005.pdf)
|
||||
|
||||
## Fluency is measured in Words Correct Per Minute.
|
||||
|
||||
A student reads a story with <TotalNumberOfWords/> words in <TotalAmountOfTime/>. She made <TotalNumberOfErrors/> errors. To determine WCPM (Words Correct Per Minute):
|
||||
|
||||
1. <p>Count the total number of words.</p>
|
||||
<pre><code><TotalNumberOfWords/> words</code></pre>
|
||||
2. <p>Count the number of mistakes.</p>
|
||||
<pre><code><TotalNumberOfErrors/> mistakes</code></pre>
|
||||
3. <p>Take the number of words minus the number of mistakes = number of words read correctly.</p>
|
||||
<pre><code><TotalNumberOfWords/> words - <TotalNumberOfErrors/> mistakes = <TotalNumberOfCorrectWords/> correct words</code></pre>
|
||||
4. <p>Convert the time it took to read the passage to seconds.</p>
|
||||
<pre><code><TotalAmountOfTime/> to read = <TotalAmountOfTime as="seconds"/>s</code></pre>
|
||||
5. <p>Convert the number of seconds to a decimal by dividing the number of seconds by 60. This is the total reading time.</p>
|
||||
<pre><code><TotalAmountOfTime as="seconds"/>s / 60 = <TotalAmountOfTime as="decimal"/> total reading time</code></pre>
|
||||
6. <p>Divide the number of words read correctly by the total reading time in decimal form.</p>
|
||||
<pre><code><TotalNumberOfCorrectWords/> / <TotalAmountOfTime as="decimal"/> = <TotalWordsCorrectPerMinute/> Words Correct per Minute (WCPM)</code></pre>
|
||||
|
||||
</Right>
|
||||
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
|
||||
<Left>
|
||||
|
||||
## The Shaver-Troup Formulations
|
||||
|
||||
As an Educational Therapist, Dr. Bonnie Shaver-Troup observed that reading issues masked the individual’s true capability and intelligence. Shaver-Troup theorized that these issues were a sensitivity to typographical factors. She began manipulating multiple text factors to find a match between text format and an individual’s optimized visual processing capabilities.
|
||||
|
||||
In clinical practice with both children and adults, Dr. Shaver-Troup used a fluency test to measure the effectiveness of the solution. Test results supported her theory; making the modifications to typography allowed the reader to instantaneously improve Words Correct per Minute (WCPM) scores, which research correlates to increased comprehension.
|
||||
|
||||
### The Shaver-Troup Formulations Examined
|
||||
Three factors were manipulated in increasing intensity. These factors were:
|
||||
|
||||
- Hyper expansion of character spacing
|
||||
- Expanded font-outline shapes
|
||||
- Sans-serif font to reduce noise
|
||||
|
||||
### The Demonstration of Effectiveness
|
||||
|
||||
20 third graders, eight males and twelve females, read for one minute in five fonts.
|
||||
|
||||
All text was set at 16pt and the reading materials were two grade levels above the participants current grade level to ensure the typography was being measured, rather than reading competency.
|
||||
|
||||
Each student read out loud a passage set in a control of Times New Roman, then four of the Lexend Series — Regular, Deca, Mega, and Giga.
|
||||
|
||||
</Left>
|
||||
|
||||
<Right sticky top="1vw">
|
||||
|
||||
<Tester weight="regular" />
|
||||
<Tester weight="deca"/>
|
||||
<Tester weight="mega"/>
|
||||
<Tester weight="giga"/>
|
||||
|
||||
</Right>
|
||||
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
|
||||
<Left sticky height="76vh">
|
||||
|
||||
<Page>
|
||||
<ReadingSimulator/>
|
||||
</Page>
|
||||
</Left>
|
||||
|
||||
<Right width="50%">
|
||||
<Chart/>
|
||||
</Right>
|
||||
|
||||
</Flex>
|
||||
|
||||
<ChartStats>
|
||||
|
||||
17/19 had better scores with Lexend over Times New Roman
|
||||
|
||||
1 could not be measured
|
||||
|
||||
2 had the best score with Times New Roman
|
||||
|
||||
2 had the best score with Lexend Regular
|
||||
|
||||
1 had the best score with Lexend Deca
|
||||
|
||||
9 had the best score with Lexend Mega
|
||||
|
||||
5 had the best score with Lexend Giga
|
||||
|
||||
|
||||
110 Avg WCPM for Times New Roman
|
||||
|
||||
128 Avg WCPM for Lexend
|
||||
|
||||
+19.8% Avg Improvement in WCPM
|
||||
|
||||
</ChartStats>
|
||||
|
||||
<Flex>
|
||||
<Left>
|
||||
|
||||
## A Virtue of Variable Font Technology
|
||||
|
||||
As the study demonstrates, while the Lexend series were beneficial to a large sample of students, no one setting worked best for all students. Diverse readers need different axis settings like people require different eyeglass prescriptions.
|
||||
|
||||
Eyeglass prescriptions are not six strict settings. There are more granular settings possible.
|
||||
|
||||
Variable font technology allows for continuous selection of the Lexend Series to find the specific setting for an individual student.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<Tester/>
|
||||
</Left>
|
||||
<Right sticky>
|
||||
<Page>
|
||||
<ReadingSimulator family="Lexend" disabled={['family']}/>
|
||||
</Page>
|
||||
</Right>
|
||||
</Flex>
|
||||
|
||||
<Intro>
|
||||
|
||||
|
||||
# Speculative Interlude
|
||||
|
||||
At the end of the 2018 project, Thomas identified research by Dr. Nadine Chahine on reading proficiency in Arabic. The conclusion and implication of her research support the reasonable proposal to apply the Shaver-Troup Formulation into an Arabic variable font.
|
||||
|
||||
Aspects of hyper-extension are already familiar in Arabic. The lengthening of text for justification is named kashida which can aid in legibility:
|
||||
|
||||
>The kashida is used to give a better character layout on the baseline, and to lessen the cluttering at the joint point between two successive letters of the same word
|
||||
|
||||
###### Mohamed Jamal Eddine Benatia,Mohamed Elyaakoubi and Azzeddine Lazrek [Source](http://www.tug.org/TUGboat/tb27-2/tb87benatia.pdf)
|
||||
|
||||
Working with Dr. Chahine and support from Google, the Lexend team wishes to investigate if the Shaver-Troup Formulations applied to the Arabic script has the same beneficial fluency properties as we have demonstrated in the Latin script. If proven true, the implications are the following:
|
||||
|
||||
- A richer understanding of human perception that is cross-cultural in scope
|
||||
- Demonstration of evidence-based design practices in regard to legibility
|
||||
- A contribution to expanding reading-proficiency around the world
|
||||
|
||||
<ArabicExample/>
|
||||
|
||||
## Stay Connected
|
||||
|
||||
For updates when Lexend will be available on Google Fonts, news on original research, and to contribute, sign up to our newsletter.
|
||||
|
||||
|
||||
</Intro>
|
||||
|
||||
<Form/>
|
||||
|
||||
export default ({ children }) => (
|
||||
<Layout>
|
||||
<TypeChoicesProvider>
|
||||
<ReadingSimulatorProvider>
|
||||
{children}
|
||||
</ReadingSimulatorProvider>
|
||||
</TypeChoicesProvider>
|
||||
</Layout>
|
||||
)
|
43
readme.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Lexend Website
|
||||
|
||||
<img width="1093" alt="Screen Shot 2019-06-08 at 7 26 26 PM" src="https://user-images.githubusercontent.com/25366/59153730-83bfec80-8a2f-11e9-8c25-a188c906e3ed.png">
|
||||
|
||||
An awesome, function website for an awesome, functional font — Lexend is a font designed specifically to increase to reading-proficiency, and it's got some data to back it up. We designed this sucker to show it.
|
||||
|
||||
There's a lot of fun but not-too-complicated tech here:
|
||||
- [next.js](https://nextjs.org) for a minimal react framework
|
||||
- [react-spring](https://www.react-spring.io/) for amazing animation
|
||||
- [emotion](https://emotion.sh/docs/introduction) for simple & mostly-contained component styling
|
||||
- [mdx](https://mdxjs.com/) to keep our homepage in fairly simple markdown, with components mixed in
|
||||
|
||||
### 🏃♂️ Getting Up & Running
|
||||
|
||||
> Make sure you've got [Node](https://nodejs.org/en/) installed, cuz we usin' JavaScript. I prefer [yarn](https://yarnpkg.com/en/) to install dependencies, but [npm](https://www.npmjs.com/get-npm) works, too. You just need one or the other — the commands are the same, switch `yarn` with `npm` if that's what you've got.
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
2. Run locally:
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
2. Make some changes, #whatevs, commit to the repo
|
||||
3. Deploy to Github Pages:
|
||||
```bash
|
||||
yarn deploy
|
||||
```
|
||||
|
||||
The deploy command simplifies the gh-pages deploy process using the lovely [gh-pages](https://github.com/tschaub/gh-pages) library, which is included in the development dependencies when you `yarn install` and uses `git` behind the scenes.
|
||||
|
||||
#### 👨🏻🚒 Gotchas to Watch Out For in Future Development
|
||||
|
||||
###### Deployment
|
||||
|
||||
One gotcha (which you might notice in `package.json` on #master and shouldn't have to worry about if you use as-is), is that we have to pass the `-t` argument, which is shortcode for _including dotfiles_ — gh-pages ignores folders that start with an `_` if you don't include a `.nojekyll` file, and we're making sure to include that shnaz in the `predeploy` command that gets automatically run when you hit `yarn deploy`.
|
||||
|
||||
###### Static Files
|
||||
|
||||
The `/static` folder is an important part of Next.js, and where we store all our non-dynamic files like fonts, images, etc. Because gh-pages is running in a subdirectory (`micahbrich.github.io/lexend` instead of just `micahbrich.github.io` or `lexend.com`), we have to change the URLs for static files to match whatever directory it's living in.
|
||||
|
||||
You'll notice all the static files are absolute URLs, just to make it simple. If you change where this lives, you'll want to do a find & replace to update those static files. There aren't a lot.
|
BIN
static/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
static/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
9
static/favicon/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="https://micahbrich.github.io/lexend/static/favicon/mstile-150x150.png"/>
|
||||
<TileColor>#b91d47</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
static/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 661 B |
BIN
static/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 880 B |
BIN
static/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/favicon/mstile-144x144.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
static/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
static/favicon/mstile-310x150.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/favicon/mstile-310x310.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
static/favicon/mstile-70x70.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
27
static/favicon/safari-pinned-tab.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M934 6389 c-152 -19 -293 -148 -320 -294 -4 -22 -7 -1194 -7 -2605 0
|
||||
-2721 -2 -2589 45 -2683 27 -54 95 -125 142 -149 105 -53 -88 -50 2706 -49
|
||||
l2585 1 63 22 c122 43 221 165 239 298 5 30 8 1196 7 2590 -1 2721 2 2591 -51
|
||||
2686 -46 82 -155 162 -245 179 -43 8 -5098 12 -5164 4z m1252 -2722 l0 -882
|
||||
487 -3 487 -2 0 -155 0 -155 -647 0 -648 0 1 1032 c1 568 2 1037 3 1041 0 4
|
||||
72 7 159 7 l158 -2 0 -881z m1928 488 c148 -217 272 -392 275 -390 3 2 71 103
|
||||
151 224 80 122 195 297 255 389 l110 167 188 3 c150 2 187 0 181 -10 -7 -12
|
||||
-45 -69 -281 -413 -178 -260 -287 -420 -357 -523 -52 -76 -68 -107 -61 -117
|
||||
27 -39 708 -973 720 -988 8 -9 15 -19 15 -22 0 -3 -91 -5 -202 -5 l-203 1 -85
|
||||
124 c-230 337 -462 670 -468 672 -4 1 -59 -75 -122 -168 -63 -93 -144 -212
|
||||
-180 -264 -36 -52 -107 -155 -158 -230 l-93 -135 -185 0 c-101 0 -184 3 -184
|
||||
8 0 4 29 46 63 94 34 48 94 132 132 187 39 56 113 161 165 235 52 74 121 172
|
||||
153 218 31 46 95 137 141 202 l83 120 -137 190 c-75 105 -229 319 -341 476
|
||||
-113 157 -215 300 -227 317 l-23 33 203 -1 203 -1 269 -393z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
19
static/favicon/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://micahbrich.github.io/lexend/static/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://micahbrich.github.io/lexend/static/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
BIN
static/fonts/lexendgx.ttf
Normal file
BIN
static/fonts/lexendgx.woff2
Normal file
BIN
static/images/noordzji-cover-cut.png
Normal file
After Width: | Height: | Size: 703 KiB |
BIN
static/images/noordzji-cover.jpg
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
static/social/lexend.jpg
Normal file
After Width: | Height: | Size: 216 KiB |