├── .gitignore ├── .nova ├── Artwork ├── Configuration.json └── Tasks │ ├── Develop.json │ └── Package.json ├── README.md ├── craco.config.js ├── package.json ├── public ├── favicon-128.png ├── favicon-152.png ├── favicon-167.png ├── favicon-180.png ├── favicon-196.png ├── favicon-32.png ├── index.html ├── manifest.json ├── ogimage.png └── robots.txt ├── src ├── App.tsx ├── Banner.tsx ├── BikePicker.tsx ├── BlockLink.tsx ├── Char.tsx ├── CodeExample.tsx ├── Dialog.tsx ├── DialogElement.tsx ├── Example.tsx ├── Heading.tsx ├── Paragraph.tsx ├── Performer.tsx ├── RewindEffect.tsx ├── RewindListener.tsx ├── Section.tsx ├── SectionFocusContext.tsx ├── SkipEffect.tsx ├── SmashEffect.tsx ├── StaticCodeExample.tsx ├── Subheading.tsx ├── TextPanel.tsx ├── colours.ts ├── content │ ├── APIDocs.tsx │ ├── Chat.tsx │ ├── Guides.tsx │ └── QuickStart.tsx ├── guides │ ├── Accessibility.tsx │ ├── ChangingValues.tsx │ ├── Effects.tsx │ ├── GuidesHelp.tsx │ ├── HookIntro.tsx │ ├── Install.tsx │ ├── Linebreaking.tsx │ ├── LinebreakingWithStyle.tsx │ ├── SkipRewind.tsx │ ├── StyledText.tsx │ ├── StylingCharacters.tsx │ ├── Timing.tsx │ └── WindupsWithAnything.tsx ├── images │ ├── auntie.svg │ ├── compass-menu.svg │ ├── forks.svg │ ├── frog-menu.svg │ ├── frog │ │ ├── Group 12-1.svg │ │ ├── Group 12-2.svg │ │ ├── Group 12.svg │ │ ├── Group 13-1.svg │ │ ├── Group 13-2.svg │ │ ├── Group 13.svg │ │ ├── f-cool-open-1.svg │ │ ├── f-cool-open-2.svg │ │ ├── f-cool-resting.svg │ │ ├── f-laff-open-1.svg │ │ ├── f-laff-open-2.svg │ │ ├── f-laff-resting.svg │ │ ├── f-mad-open-1.svg │ │ ├── f-mad-open-2.svg │ │ ├── f-mad-resting.svg │ │ ├── f-norm-open-1.svg │ │ ├── f-norm-open-2.svg │ │ ├── f-norm-resting.svg │ │ ├── f-shame-open-1.svg │ │ ├── f-shame-open-2.svg │ │ ├── f-shame-resting.svg │ │ ├── f-shock-open-1.svg │ │ ├── f-shock-open-2.svg │ │ ├── f-shock-resting.svg │ │ ├── f-smug-open-1.svg │ │ ├── f-smug-open-2.svg │ │ └── f-smug-resting.svg │ ├── gwilco.svg │ ├── key-a.svg │ ├── key-b.svg │ ├── key-c.svg │ ├── key-d.svg │ ├── key-menu.svg │ ├── keyboard-menu.svg │ ├── little-hand.svg │ ├── mega-yaki.svg │ ├── nexters.svg │ ├── point.svg │ ├── repo-menu.svg │ ├── ruffle-pizza.svg │ └── snowman-kebab.svg ├── index.tsx ├── performers │ └── Frog.tsx ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .linaria_cache 26 | -------------------------------------------------------------------------------- /.nova/Artwork: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/.nova/Artwork -------------------------------------------------------------------------------- /.nova/Configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.use_scm_ignored_files" : true, 3 | "workspace.art_style" : 1, 4 | "workspace.color" : 9, 5 | "workspace.name" : "Windups Docs", 6 | "workspace.preview_append_paths" : false, 7 | "workspace.preview_type" : "custom", 8 | "workspace.preview_url" : "http:\/\/localhost:3000" 9 | } 10 | -------------------------------------------------------------------------------- /.nova/Tasks/Develop.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions" : { 3 | "run" : { 4 | "enabled" : true, 5 | "script" : "yarn start" 6 | } 7 | }, 8 | "identifier" : "00C30011-A45F-4675-841A-F1EFB3E34F3B", 9 | "openLogOnRun" : "start", 10 | "persistent" : true 11 | } -------------------------------------------------------------------------------- /.nova/Tasks/Package.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions" : { 3 | "build" : { 4 | "enabled" : true, 5 | "script" : "yarn build" 6 | } 7 | }, 8 | "identifier" : "5223FE37-D050-4208-8C24-09849B29074C", 9 | "openLogOnRun" : "finish" 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # windups docs 2 | 3 | This is a React app that acts as documentation for the [windups](https://github.com/sgwilym/windups) library. 4 | 5 | You can see the docs site in action at https://windups.gwil.co 6 | 7 | Building that library and its docs was a solo project, and at a certain point my focus was getting it out the door — so this repo might seem a bit like a teenager's bedroom! Which is to say, some things in this code base are the way they are because I thought I'd be the only person to look at them. 8 | 9 | That said it's an extensive example of a real world application of the windups library, with lots of little tricks to make the effect more user-friendly: stuff like auto-scrolling the page to keep up with text that's being typed out; animating character portraits next to text; special effects. 10 | 11 | There is also a pretty full-fledged dialogue system in here that could probably be pulled out into its own family of components. Some day. -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoLinariaPlugin = require("craco-linaria"); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoLinariaPlugin, 7 | options: { 8 | // Linaria options 9 | } 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windup-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "5.5.0", 7 | "@reach/visually-hidden": "0.10.1", 8 | "@rehooks/component-size": "1.0.3", 9 | "@rooks/use-key": "3.4.3", 10 | "@types/jest": "24.0.18", 11 | "@types/node": "12.7.12", 12 | "@types/react": "16.9.5", 13 | "@types/react-dom": "16.9.1", 14 | "linaria": "1.3.1", 15 | "prism-react-renderer": "1.1.0", 16 | "react": "16.10.2", 17 | "react-dom": "16.10.2", 18 | "react-intersection-observer": "8.25.2", 19 | "react-router": "5.1.2", 20 | "react-router-dom": "5.1.2", 21 | "react-router-hash-link": "1.2.2", 22 | "react-scripts": "3.3.0", 23 | "resize-observer-polyfill": "1.5.1", 24 | "scroll-behavior-polyfill": "2.0.13", 25 | "typescript": "3.7.2", 26 | "use-debounce": "3.4.1", 27 | "use-internet-time": "1.0.1", 28 | "windups": "^1.1.7" 29 | }, 30 | "scripts": { 31 | "start": "craco start", 32 | "build": "craco build", 33 | "test": "craco test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@types/react-router-dom": "^5.1.3", 53 | "@types/react-router-hash-link": "^1.2.1", 54 | "core-js": "2", 55 | "craco-linaria": "^1.1.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-128.png -------------------------------------------------------------------------------- /public/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-152.png -------------------------------------------------------------------------------- /public/favicon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-167.png -------------------------------------------------------------------------------- /public/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-180.png -------------------------------------------------------------------------------- /public/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-196.png -------------------------------------------------------------------------------- /public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/favicon-32.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | windups 27 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /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/ogimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgwilym/windups-docs/809404d6215ba515cebe6bc4137fc3d18ea90dcf/public/ogimage.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/Banner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useWindupString } from "windups"; 3 | import { cx, css } from "linaria"; 4 | import KeyA from "./images/key-a.svg"; 5 | import KeyB from "./images/key-b.svg"; 6 | import KeyC from "./images/key-c.svg"; 7 | import KeyD from "./images/key-d.svg"; 8 | 9 | const keyRootStyle = css` 10 | width: 76px; 11 | display: flex; 12 | justify-content: right; 13 | margin: 0 12px; 14 | `; 15 | 16 | type KeyProps = { 17 | changeValue: any; 18 | }; 19 | 20 | const Key: React.FC = ({ changeValue }) => { 21 | const [[src], next] = React.useReducer( 22 | (state) => { 23 | const [first, ...rest] = state; 24 | return [...rest, first]; 25 | }, 26 | [KeyD, KeyA, KeyB, KeyC] 27 | ); 28 | 29 | React.useEffect(() => { 30 | next("whatever"); 31 | }, [changeValue]); 32 | 33 | return ( 34 |
35 | 36 |
37 | ); 38 | }; 39 | 40 | const bannerRootStyle = css` 41 | flex: 1 0 auto; 42 | display: flex; 43 | align-items: center; 44 | justify-content: center; 45 | margin-bottom: 48px; 46 | `; 47 | 48 | const bannerStyle = css` 49 | color: black; 50 | border-radius: 6px; 51 | border: 4px black solid; 52 | transform: skew(-5deg); 53 | padding: 12px; 54 | font-size: 64px; 55 | font-family: "Menlo", monospace; 56 | font-style: italic; 57 | display: inline-block; 58 | transition: width 50ms; 59 | margin-right: 24px; 60 | `; 61 | 62 | const bannerTextStyle = css` 63 | transform: skew(5deg); 64 | display: inline-block; 65 | `; 66 | 67 | const finishedBannerStyle = css` 68 | color: white; 69 | background-color: black; 70 | `; 71 | 72 | type BannerProps = { 73 | onFinished: () => void; 74 | }; 75 | 76 | const Banner: React.FC = ({ onFinished }) => { 77 | const [isFinished, setIsFinished] = React.useState(false); 78 | const [count, setCount] = React.useState(0); 79 | 80 | const [text] = useWindupString("windups", { 81 | pace: () => 150, 82 | onChar: () => { 83 | setCount((prev) => prev + 1); 84 | }, 85 | onFinished: () => { 86 | setTimeout(() => { 87 | setCount((prev) => prev + 1); 88 | onFinished(); 89 | setIsFinished(true); 90 | }, 300); 91 | }, 92 | }); 93 | 94 | return ( 95 |
96 | 97 |
98 | {text} 99 |
100 |
101 | ); 102 | }; 103 | 104 | export default Banner; 105 | -------------------------------------------------------------------------------- /src/BikePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { WindupChildren, Pace } from "windups"; 3 | import { GREEN, PINK } from "./colours"; 4 | import { css } from "linaria"; 5 | 6 | const Bicycle = ({ colour }: { colour: string }) => { 7 | return ( 8 | 15 | 16 | 21 | 28 | 32 | 38 | 39 | ); 40 | }; 41 | 42 | const buttonStyle = css` 43 | font-family: "Menlo", monospace; 44 | font-size: 1.5em; 45 | color: white; 46 | padding: 16px; 47 | border-radius: 24px; 48 | border: none; 49 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 50 | transform: skew(0deg, -3deg); 51 | transition: 300ms all; 52 | &:hover { 53 | transform: scale(1.03) skew(0deg, -3deg); 54 | } 55 | `; 56 | 57 | const Button = ({ 58 | colour, 59 | label, 60 | onClick, 61 | }: { 62 | colour: string; 63 | label: string; 64 | onClick: () => void; 65 | }) => { 66 | return ( 67 | 74 | ); 75 | }; 76 | 77 | const rootStyle = css` 78 | display: flex; 79 | flex-direction: column; 80 | align-items: stretch; 81 | `; 82 | 83 | const BikePicker = () => { 84 | const [colour, setColour] = useState("black"); 85 | 86 | return ( 87 |
88 | 89 | 90 | 91 |
109 | ); 110 | }; 111 | 112 | export default BikePicker; 113 | -------------------------------------------------------------------------------- /src/BlockLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LinkProps } from "react-router-dom"; 3 | import { css, cx } from "linaria"; 4 | import { TEXT_PINK, GREEN } from "./colours"; 5 | import { HashLink as Link } from "react-router-hash-link"; 6 | import LittleHand from "./images/little-hand.svg"; 7 | 8 | const rootStyle = css` 9 | font-family: Menlo, monospace; 10 | display: inline-block; 11 | 12 | border-radius: 5px; 13 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 14 | padding: 8px; 15 | text-decoration-thickness: 2px; 16 | 17 | transition: all 200ms; 18 | color: white; 19 | position: relative; 20 | 21 | &:hover { 22 | background: white; 23 | } 24 | 25 | &:hover:before { 26 | content: url(${LittleHand}); 27 | position: absolute; 28 | 29 | top: 50%; 30 | transform: translate(-110%, -45%); 31 | } 32 | 33 | img { 34 | flex: 0 0 auto; 35 | margin-right: 0.5em; 36 | filter: invert(100%); 37 | transition: all 200ms; 38 | } 39 | 40 | &:hover img { 41 | filter: none; 42 | } 43 | `; 44 | 45 | const blackStyle = css` 46 | background: black; 47 | 48 | &:hover { 49 | color: black; 50 | } 51 | `; 52 | 53 | const greenStyle = css` 54 | background: ${GREEN}; 55 | 56 | &:hover { 57 | color: ${GREEN}; 58 | } 59 | `; 60 | 61 | const pinkStyle = css` 62 | background: ${TEXT_PINK}; 63 | 64 | &: hover { 65 | color: ${TEXT_PINK}; 66 | } 67 | `; 68 | 69 | const innerStyle = css` 70 | display: flex; 71 | align-items: center; 72 | `; 73 | 74 | const themeStyles = { 75 | PINK: pinkStyle, 76 | GREEN: greenStyle, 77 | BLACK: blackStyle 78 | }; 79 | 80 | type BlockLinkProps = { 81 | theme?: "PINK" | "GREEN" | "BLACK"; 82 | } & LinkProps; 83 | 84 | const BlockLink: React.FC = ({ 85 | children, 86 | theme = "PINK", 87 | className, 88 | ...rest 89 | }) => { 90 | return ( 91 | 92 |
{children}
93 | 94 | ); 95 | }; 96 | 97 | export default BlockLink; 98 | -------------------------------------------------------------------------------- /src/Char.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { css, cx } from "linaria"; 3 | import { CharWrapper } from "windups"; 4 | import { TEXT_PINK, GREY } from "./colours"; 5 | 6 | export const CharContext = React.createContext({ animated: false }); 7 | 8 | export const CHAR_FONT_STYLE = "14pt 'Menlo'"; 9 | 10 | const charStyle = css` 11 | display: inline-block; 12 | font: ${CHAR_FONT_STYLE}; 13 | transform: translateZ(0); 14 | `; 15 | 16 | const animatingStyle = css` 17 | @keyframes enter { 18 | from { 19 | opacity: 0; 20 | transform: scale(0.1) rotate(-80deg); 21 | } 22 | to { 23 | opacity: 1; 24 | } 25 | } 26 | animation-name: enter; 27 | animation-duration: 100ms; 28 | `; 29 | 30 | export const StandardChar: React.FC = ({ children }) => { 31 | const { animated } = useContext(CharContext); 32 | 33 | return ( 34 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | const dropStyle = css` 41 | @keyframes enter { 42 | 0% { 43 | opacity: 0; 44 | } 45 | 50% { 46 | opacity: 1; 47 | } 48 | 25% { 49 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 50 | transform: translate3d(0, -100%, 0); 51 | transform-style: preserve-3d; 52 | } 53 | 0%, 54 | 50%, 55 | 88%, 56 | 96%, 57 | 100% { 58 | animation-timing-function: cubic-bezier(0.12, 0.52, 0.57, 1); 59 | transform: translate3d(0, 0, 0); 60 | transform-style: preserve-3d; 61 | } 62 | 75% { 63 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 64 | transform: translate3d(0, -33%, 0); 65 | transform-style: preserve-3d; 66 | } 67 | 94% { 68 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 69 | transform: translate3d(0, -11%, 0); 70 | transform-style: preserve-3d; 71 | } 72 | 97% { 73 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 74 | transform: translate3d(0, -3%, 0); 75 | transform-style: preserve-3d; 76 | } 77 | } 78 | animation-name: enter; 79 | animation-duration: 500ms; 80 | `; 81 | 82 | const emphasisStyle = css` 83 | color: ${TEXT_PINK}; 84 | `; 85 | 86 | const thinkingStyle = css` 87 | @keyframes fade { 88 | 0% { 89 | opacity: 0; 90 | } 91 | 100% { 92 | opacity: 0.7; 93 | } 94 | } 95 | animation-name: fade; 96 | animation-duration: 500ms; 97 | opacity: 0.7; 98 | `; 99 | 100 | const EmphasisChar: React.FC = ({ children }) => { 101 | const { animated } = useContext(CharContext); 102 | 103 | return ( 104 | 105 | {children} 106 | 107 | ); 108 | }; 109 | 110 | export const Emphasis: React.FC = ({ children }) => { 111 | return {children}; 112 | }; 113 | 114 | const ThinkingChar: React.FC = ({ children }) => { 115 | return {children}; 116 | }; 117 | 118 | export const Thinking: React.FC = ({ children }) => { 119 | return {children}; 120 | }; 121 | 122 | const DropChar: React.FC = ({ children }) => { 123 | const { animated } = useContext(CharContext); 124 | 125 | return ( 126 | {children} 127 | ); 128 | }; 129 | 130 | export const Dropped: React.FC = ({ children }) => { 131 | return {children}; 132 | }; 133 | -------------------------------------------------------------------------------- /src/CodeExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { DialogChildContext, DialogContext } from "./Dialog"; 3 | import { textFromChildren, useWindupString } from "windups"; 4 | import { css, cx } from "linaria"; 5 | import { GREEN } from "./colours"; 6 | import Highlight, { defaultProps } from "prism-react-renderer"; 7 | import theme from "prism-react-renderer/themes/nightOwl"; 8 | import useKey from "@rooks/use-key"; 9 | import SectionFocusContext from "./SectionFocusContext"; 10 | import { SectionContext } from "./Section"; 11 | 12 | const rootStyle = css` 13 | font: 16px "Menlo", monospace; 14 | padding: 16px; 15 | border: 2px solid #e6e6e6; 16 | border-radius: 5px; 17 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 18 | margin-bottom: 16px; 19 | white-space: pre; 20 | color: ${GREEN}; 21 | background-color: black; 22 | line-height: 1.5; 23 | overflow: auto; 24 | `; 25 | 26 | function randomInt(max: number) { 27 | return Math.floor(Math.random() * Math.floor(max)); 28 | } 29 | 30 | type CodeExampleProps = { 31 | children: string; 32 | }; 33 | 34 | const CodeExample: React.FC = ({ children }) => { 35 | const { proceed, isActive } = useContext(DialogChildContext); 36 | const { isFinished: dialogIsFinished } = useContext(DialogContext); 37 | const { activeSectionID } = useContext(SectionFocusContext); 38 | const { id } = useContext(SectionContext); 39 | const isTotallyActive = isActive && activeSectionID === id; 40 | const [windup, { skip, isFinished }] = useWindupString(children, { 41 | onFinished: () => { 42 | setTimeout(() => { 43 | proceed(); 44 | }, 500); 45 | }, 46 | pace: (char) => { 47 | if (char === "\n") { 48 | return 200; 49 | } 50 | return randomInt(80); 51 | }, 52 | skipped: dialogIsFinished, 53 | }); 54 | 55 | useKey([13, 39], () => { 56 | if (isTotallyActive && !isFinished) { 57 | skip(); 58 | } else { 59 | if (isTotallyActive && !dialogIsFinished) { 60 | proceed(); 61 | } 62 | } 63 | }); 64 | 65 | return ( 66 | 67 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 68 |
69 |           {tokens.map((line, i) => (
70 |             
71 | {line.map((token, key) => ( 72 | 73 | ))} 74 |
75 | ))} 76 |
77 | )} 78 |
79 | ); 80 | }; 81 | 82 | export default CodeExample; 83 | -------------------------------------------------------------------------------- /src/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useCallback, 4 | useRef, 5 | useEffect, 6 | useContext, 7 | } from "react"; 8 | import { useInView } from "react-intersection-observer"; 9 | import useSize from "@rehooks/component-size"; 10 | import SectionFocusContext from "./SectionFocusContext"; 11 | import { SectionContext } from "./Section"; 12 | 13 | if (!("scrollBehavior" in document.documentElement.style)) { 14 | import("scroll-behavior-polyfill"); 15 | } 16 | 17 | if (!("ResizeObserver" in window)) { 18 | (global as any).ResizeObserver = import("resize-observer-polyfill"); 19 | } 20 | 21 | export const DialogContext = React.createContext({ 22 | isFinished: false, 23 | }); 24 | 25 | export const DialogChildContext = React.createContext({ 26 | isActive: true, 27 | proceed: () => {}, 28 | }); 29 | 30 | function useKeepInViewer(height: number) { 31 | const measurementRef = useRef(); 32 | const prevHeightRef = useRef(height); 33 | const { isActive } = useContext(SectionContext); 34 | 35 | const [inViewRef, isInView] = useInView({ 36 | rootMargin: "-100px 0px", 37 | }); 38 | 39 | const setRef = useCallback( 40 | (node) => { 41 | measurementRef.current = node; 42 | inViewRef(node); 43 | }, 44 | [inViewRef] 45 | ); 46 | 47 | useEffect(() => { 48 | if (!measurementRef.current) { 49 | return; 50 | } 51 | 52 | // only scroll there within a certain threshold.... 53 | 54 | const scrollBottom = window.scrollY + window.innerHeight; 55 | const bottomPos = 56 | measurementRef.current.offsetTop + measurementRef.current.offsetHeight; 57 | 58 | const isJustOutOfView = Math.abs(scrollBottom - bottomPos) < 200; 59 | 60 | if ( 61 | !isInView && 62 | isActive && 63 | height !== prevHeightRef.current && 64 | isJustOutOfView 65 | ) { 66 | window.scroll({ 67 | top: measurementRef.current.offsetTop - (window.innerHeight / 3) * 2, 68 | behavior: "smooth", 69 | }); 70 | } 71 | 72 | prevHeightRef.current = height; 73 | }, [isInView, isActive, height]); 74 | 75 | return
; 76 | } 77 | 78 | const Dialog: React.FC<{ skipped?: boolean }> = ({ 79 | children, 80 | skipped = false, 81 | }) => { 82 | const [numberOfChildrenToShow, setNumberOfChildrenToShow] = useState(1); 83 | const activeChildIndex = numberOfChildrenToShow - 1; 84 | const rootRef = useRef(null); 85 | const { height } = useSize(rootRef); 86 | const { activeSectionID } = useContext(SectionFocusContext); 87 | const { id, hasSkipped } = useContext(SectionContext); 88 | const isDialogActive = activeSectionID === id; 89 | const keepy = useKeepInViewer(height); 90 | const finish = useCallback(() => { 91 | setNumberOfChildrenToShow(React.Children.count(children)); 92 | }, [children]); 93 | 94 | useEffect(() => { 95 | if (skipped) { 96 | finish(); 97 | } 98 | }, [skipped, finish]); 99 | 100 | useEffect(() => { 101 | if (hasSkipped) { 102 | finish(); 103 | } 104 | }, [hasSkipped, finish]); 105 | 106 | const shownChildren = React.Children.toArray(children) 107 | .slice(0, numberOfChildrenToShow) 108 | .map((child, i) => ( 109 | { 114 | if (i + 2 > numberOfChildrenToShow && isDialogActive) { 115 | setNumberOfChildrenToShow(i + 2); 116 | } 117 | }, 118 | }} 119 | > 120 | {child} 121 | 122 | )); 123 | 124 | return ( 125 | = React.Children.count(children), 128 | }} 129 | > 130 |
131 | {shownChildren} 132 | {keepy} 133 |
134 |
135 | ); 136 | }; 137 | 138 | export default Dialog; 139 | -------------------------------------------------------------------------------- /src/DialogElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import TextPanel from "./TextPanel"; 3 | import { DialogContext, DialogChildContext } from "./Dialog"; 4 | import useKey from "@rooks/use-key"; 5 | import { useIsFinished, useSkip, Effect } from "windups"; 6 | import { SectionContext } from "./Section"; 7 | import SectionFocusContext from "./SectionFocusContext"; 8 | import { css } from "linaria"; 9 | import Nexters from "./images/nexters.svg"; 10 | import { GREEN } from "./colours"; 11 | 12 | export const NextListener: React.FC = () => { 13 | const isFinished = useIsFinished(); 14 | const skip = useSkip(); 15 | const { isFinished: dialogIsFinished } = useContext(DialogContext); 16 | const { proceed, isActive } = useContext(DialogChildContext); 17 | const { activeSectionID } = useContext(SectionFocusContext); 18 | const { id } = useContext(SectionContext); 19 | const isTotallyActive = isActive && activeSectionID === id; 20 | 21 | useKey([13, 39], () => { 22 | if (isTotallyActive && !isFinished) { 23 | skip(); 24 | } else { 25 | if (isTotallyActive && !dialogIsFinished) { 26 | proceed(); 27 | } 28 | } 29 | }); 30 | 31 | return null; 32 | }; 33 | 34 | const nextRootStyles = css` 35 | @keyframes drift { 36 | 100% { 37 | background-position: 111px 73px; 38 | } 39 | } 40 | @keyframes fade-in { 41 | from { 42 | opacity: 0; 43 | } 44 | to { 45 | opacity: 1; 46 | } 47 | } 48 | animation-name: drift, fade-in; 49 | animation-duration: 5s, 200ms; 50 | animation-iteration-count: infinite, 1; 51 | animation-timing-function: linear; 52 | display: block; 53 | height: 48px; 54 | border-radius: 5px; 55 | border: 2px solid #e5e5e5; 56 | background-image: url(${Nexters}); 57 | background-color: white; 58 | width: 100%; 59 | background-size: 111px 73px; 60 | font-size: 1em; 61 | font-family: "Menlo", monospace; 62 | margin-top: 16px; 63 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 64 | transition: transform 200ms; 65 | appearance: none; 66 | 67 | &:hover { 68 | transform: scale(1.02); 69 | } 70 | 71 | &:active { 72 | transform: scale(0.98); 73 | } 74 | 75 | &:focus { 76 | color: ${GREEN}; 77 | border-color: ${GREEN}; 78 | } 79 | `; 80 | 81 | export const NextButton: React.FC = () => { 82 | const { setActiveSectionID } = useContext(SectionFocusContext); 83 | const { id } = useContext(SectionContext); 84 | const { isFinished: dialogIsFinished } = useContext(DialogContext); 85 | const { proceed, isActive } = useContext(DialogChildContext); 86 | 87 | return !dialogIsFinished && isActive ? ( 88 | 98 | ) : null; 99 | }; 100 | 101 | export type DialogElementProps = { 102 | autoProceed?: boolean; 103 | }; 104 | 105 | const DialogElement: React.FC = ({ 106 | children, 107 | autoProceed 108 | }) => { 109 | const { proceed } = useContext(DialogChildContext); 110 | 111 | return ( 112 | <> 113 | 114 | 115 |
{children}
116 | {autoProceed ? : } 117 |
118 | 119 | ); 120 | }; 121 | 122 | export default DialogElement; 123 | -------------------------------------------------------------------------------- /src/Example.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { css } from "linaria"; 3 | import { DialogChildContext } from "./Dialog"; 4 | 5 | const rootStyle = css` 6 | font: 16px "Menlo", monospace; 7 | padding: 16px; 8 | border: 2px solid #e6e6e6; 9 | border-radius: 5px; 10 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 11 | margin-bottom: 16px; 12 | background-color: white; 13 | `; 14 | 15 | const Example: React.FC = ({ children }) => { 16 | const { proceed, ...rest } = useContext(DialogChildContext); 17 | 18 | return ( 19 | { 22 | setTimeout(() => { 23 | proceed(); 24 | }, 500); 25 | }, 26 | ...rest, 27 | }} 28 | > 29 |
{children}
30 |
31 | ); 32 | }; 33 | 34 | export default Example; 35 | -------------------------------------------------------------------------------- /src/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from "linaria"; 2 | import React from "react"; 3 | import { WindupChildren, CharWrapper, textFromChildren } from "windups"; 4 | 5 | const rootStyle = css` 6 | grid-column: 1/8; 7 | display: flex; 8 | flex-direction: column; 9 | `; 10 | 11 | const headingStyle = css` 12 | font-family: "Menlo", monospace; 13 | font-size: 24px; 14 | font-weight: normal; 15 | font-style: italic; 16 | border-bottom: 2px solid black; 17 | transform: skew(0, -3deg); 18 | white-space: pre; 19 | display: inline-block; 20 | align-self: flex-start; 21 | `; 22 | 23 | const headingStyleRight = css` 24 | transform: skew(0, 3deg); 25 | font-style: oblique; 26 | align-self: flex-end; 27 | `; 28 | 29 | const headingLetterStyle = css` 30 | @keyframes enter { 31 | 0% { 32 | opacity: 0; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 25% { 38 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 39 | transform: translate3d(0, -100%, 0); 40 | transform-style: preserve-3d; 41 | } 42 | 0%, 43 | 50%, 44 | 88%, 45 | 96%, 46 | 100% { 47 | animation-timing-function: cubic-bezier(0.12, 0.52, 0.57, 1); 48 | transform: translate3d(0, 0, 0); 49 | transform-style: preserve-3d; 50 | } 51 | 75% { 52 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 53 | transform: translate3d(0, -33%, 0); 54 | transform-style: preserve-3d; 55 | } 56 | 94% { 57 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 58 | transform: translate3d(0, -11%, 0); 59 | transform-style: preserve-3d; 60 | } 61 | 97% { 62 | animation-timing-function: cubic-bezier(0.4, 0, 1, 0.6); 63 | transform: translate3d(0, -3%, 0); 64 | transform-style: preserve-3d; 65 | } 66 | } 67 | animation-name: enter; 68 | animation-duration: 500ms; 69 | display: inline-block; 70 | `; 71 | 72 | const staticHeadingLetterStyle = css` 73 | display: inline-block; 74 | `; 75 | 76 | export const HeadingChar: React.FC = ({ children }) => { 77 | return ( 78 | 79 | {children} 80 | 81 | ); 82 | }; 83 | 84 | const StaticHeadingChar: React.FC = ({ children }) => { 85 | return ( 86 | 87 | {children} 88 | 89 | ); 90 | }; 91 | 92 | type HeadingProps = { 93 | onFinished?: () => void; 94 | right?: boolean; 95 | noWindup?: boolean; 96 | }; 97 | 98 | const Heading: React.FC = ({ 99 | children, 100 | onFinished, 101 | right, 102 | noWindup, 103 | }) => { 104 | const text = textFromChildren(children); 105 | 106 | return ( 107 |
108 |

109 | 110 | 114 | {children} 115 | 116 | 117 |

118 |
119 | ); 120 | }; 121 | 122 | export default Heading; 123 | -------------------------------------------------------------------------------- /src/Paragraph.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { css } from "linaria"; 3 | 4 | const rootStyle = css` 5 | font-size: 14px; 6 | font-family: "Menlo", monospace; 7 | `; 8 | 9 | const Paragraph: React.FC = ({ children }) => { 10 | return

{children}

; 11 | }; 12 | 13 | export default Paragraph; 14 | -------------------------------------------------------------------------------- /src/Performer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useReducer, useState } from "react"; 2 | import DialogElement, { DialogElementProps } from "./DialogElement"; 3 | import { css, cx } from "linaria"; 4 | import { DialogChildContext } from "./Dialog"; 5 | import { SectionContext } from "./Section"; 6 | import { useDebounce } from "use-debounce"; 7 | import { OnChar, textFromChildren, Effect } from "windups"; 8 | import VisuallyHidden from "@reach/visually-hidden"; 9 | 10 | const rootStyle = css` 11 | display: flex; 12 | align-items: flex-start; 13 | margin-bottom: 1em; 14 | `; 15 | 16 | const textStyle = css` 17 | flex: 1 0 auto; 18 | margin: 1em 0 0 1em; 19 | `; 20 | 21 | const inactiveStyle = css` 22 | opacity: 0.5; 23 | filter: grayscale(70%); 24 | `; 25 | 26 | export const PerformerContext = React.createContext({ 27 | setAvatarFrames: (_frames: FrameSet) => {}, 28 | }); 29 | 30 | type AvatorProps = { 31 | currentAvatar: string; 32 | }; 33 | 34 | const Avatar: React.FC = ({ currentAvatar }) => { 35 | const [debouncedAvatar] = useDebounce(currentAvatar, 30, { maxWait: 40 }); 36 | 37 | return {"Character; 38 | }; 39 | 40 | type Action = 41 | | { 42 | type: "next"; 43 | fromChar?: (char: string) => void; 44 | } 45 | | { 46 | type: "newFrameSet"; 47 | frameSet: FrameSet; 48 | }; 49 | 50 | function cycleFrames(frames: string[]) { 51 | const [head, ...tail] = frames; 52 | return [...tail, head]; 53 | } 54 | 55 | function avatarReducer(state: FrameSet, action: Action) { 56 | switch (action.type) { 57 | case "newFrameSet": 58 | return action.frameSet; 59 | default: 60 | return { 61 | normal: cycleFrames(state.normal), 62 | resting: cycleFrames(state.resting), 63 | }; 64 | } 65 | } 66 | 67 | function getIsResting(char: string) { 68 | switch (char) { 69 | case ".": 70 | case " ": 71 | return true; 72 | default: 73 | return false; 74 | } 75 | } 76 | 77 | export type FrameSet = { 78 | normal: string[]; 79 | resting: string[]; 80 | }; 81 | 82 | interface PerformerProps extends DialogElementProps { 83 | initialFrameSet: FrameSet; 84 | silent?: boolean; 85 | } 86 | 87 | const Performer: React.FC = ({ 88 | children, 89 | autoProceed, 90 | initialFrameSet, 91 | silent, 92 | }) => { 93 | const { isActive: sectionIsActive } = useContext(SectionContext); 94 | const { isActive } = useContext(DialogChildContext); 95 | const [frameSet, dispatch] = useReducer(avatarReducer, initialFrameSet); 96 | const text = textFromChildren(children); 97 | const [isResting, setIsResting] = useState(false); 98 | const frames = silent 99 | ? frameSet.resting 100 | : isResting 101 | ? frameSet.resting 102 | : frameSet.normal; 103 | 104 | return ( 105 | { 108 | dispatch({ type: "newFrameSet", frameSet: newFrameSet }); 109 | }, 110 | }} 111 | > 112 |
118 | 119 |
120 | {text} 121 | 122 | { 124 | setIsResting(getIsResting(char)); 125 | dispatch({ type: "next" }); 126 | }} 127 | > 128 | {children} 129 | 130 | { 132 | setIsResting(true); 133 | }} 134 | /> 135 | 136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default Performer; 143 | -------------------------------------------------------------------------------- /src/RewindEffect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRewind, Effect } from "windups"; 3 | import { useContext } from "react"; 4 | import { DialogChildContext } from "./Dialog"; 5 | 6 | const RewindEffect = () => { 7 | const rewind = useRewind(); 8 | const { proceed } = useContext(DialogChildContext); 9 | 10 | return ( 11 | { 13 | proceed(); 14 | rewind(); 15 | }} 16 | /> 17 | ); 18 | }; 19 | 20 | export default RewindEffect; 21 | -------------------------------------------------------------------------------- /src/RewindListener.tsx: -------------------------------------------------------------------------------- 1 | import { useRewind } from "windups"; 2 | import useKey from "@rooks/use-key"; 3 | import { useContext } from "react"; 4 | import { DialogChildContext } from "./Dialog"; 5 | 6 | type RewindListenerProps = { 7 | onRewind: () => void; 8 | }; 9 | 10 | const RewindListener: React.FC = ({ onRewind }) => { 11 | const { isActive } = useContext(DialogChildContext); 12 | const rewind = useRewind(); 13 | useKey( 14 | [37], 15 | isActive 16 | ? () => { 17 | onRewind(); 18 | rewind(); 19 | } 20 | : () => {} 21 | ); 22 | 23 | return null; 24 | }; 25 | 26 | export default RewindListener; 27 | -------------------------------------------------------------------------------- /src/Section.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { css, cx } from "linaria"; 3 | import SectionFocusContext from "./SectionFocusContext"; 4 | import { SubGrid, Indent1 } from "./App"; 5 | import { GREEN, PINK } from "./colours"; 6 | import { useWindupString, CharWrapper } from "windups"; 7 | import { useLocation } from "react-router"; 8 | import { HeadingChar } from "./Heading"; 9 | 10 | const dialogRoot = css` 11 | margin: 1em 0 10em 0; 12 | `; 13 | 14 | const headingRootStyle = css` 15 | grid-column: 2/8; 16 | display: flex; 17 | align-items: center; 18 | padding: 0; 19 | border: none; 20 | font-size: 1em; 21 | height: 48px; 22 | border-bottom: 2px solid black; 23 | padding: 0 0 8px 0; 24 | margin: 0 0 1em 0; 25 | background: none; 26 | justify-content: space-between; 27 | &:focus { 28 | color: ${GREEN}; 29 | } 30 | `; 31 | 32 | const activeHeadingStyle = css` 33 | border-bottom-color: ${GREEN}; 34 | `; 35 | 36 | const headingStyle = css` 37 | font-family: "Menlo", monospace; 38 | font-size: 1.2em; 39 | font-weight: normal; 40 | font-style: italic; 41 | margin: 0 0 0 0.3em; 42 | text-align: left; 43 | white-space: pre; 44 | `; 45 | 46 | const playButtonStyle = css` 47 | font-size: 2em; 48 | width: 1.3em; 49 | height: 1.3em; 50 | color: white; 51 | background-color: ${GREEN}; 52 | border-radius: 50%; 53 | padding: 2px 0 0 2px 54 | text-align: center; 55 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 56 | `; 57 | 58 | const skipButtonStyle = css` 59 | font-size: 2em; 60 | width: 2em; 61 | height: 1.3em; 62 | color: white; 63 | background-color: ${PINK}; 64 | letter-spacing: -5px; 65 | border-radius: 1.3em; 66 | padding: 2px 0 0 0 67 | text-align: center; 68 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 69 | `; 70 | 71 | export const SectionContext = React.createContext({ 72 | id: "", 73 | isActive: true, 74 | hasSkipped: false, 75 | }); 76 | 77 | type SectionProps = { 78 | title: string; 79 | id: string; 80 | alt?: boolean; 81 | }; 82 | 83 | const SectionHeading: React.FC<{ id: string; title: string }> = ({ 84 | title, 85 | id, 86 | }) => { 87 | const { hash } = useLocation(); 88 | const isHashLinked = hash === `#${id}`; 89 | const [titleWindup] = useWindupString(title, { skipped: !isHashLinked }); 90 | 91 | return ( 92 |

93 | 94 | {titleWindup} 95 | 96 |

97 | ); 98 | }; 99 | 100 | const Section: React.FC = ({ id, title, children, alt }) => { 101 | const [pressedPlay, setPressedPlay] = useState(false); 102 | const [hasSkipped, setHasSkipped] = useState(false); 103 | const { setActiveSectionID, activeSectionID } = useContext( 104 | SectionFocusContext 105 | ); 106 | const isActive = activeSectionID === id; 107 | 108 | return ( 109 | 110 | 111 | 136 |
137 | {pressedPlay && ( 138 | 139 |
{children}
140 |
141 | )} 142 | 143 | 144 | ); 145 | }; 146 | 147 | export default Section; 148 | -------------------------------------------------------------------------------- /src/SectionFocusContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SectionFocusContext = React.createContext<{ 4 | activeSectionID: string | null; 5 | setActiveSectionID: Function; 6 | }>({ 7 | activeSectionID: null, 8 | setActiveSectionID: () => {} 9 | }); 10 | 11 | export default SectionFocusContext -------------------------------------------------------------------------------- /src/SkipEffect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSkip, Effect } from "windups"; 3 | 4 | const SkipEffect = () => { 5 | const skip = useSkip(); 6 | 7 | return ; 8 | }; 9 | 10 | export default SkipEffect; 11 | -------------------------------------------------------------------------------- /src/SmashEffect.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { SFXContext } from "./App"; 3 | import { Effect } from "windups"; 4 | 5 | const SmashEffect: React.FC = () => { 6 | const { smash } = useContext(SFXContext); 7 | return ; 8 | }; 9 | 10 | export default SmashEffect; 11 | -------------------------------------------------------------------------------- /src/StaticCodeExample.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { css, cx } from "linaria"; 3 | import { LIGHT_GREY } from "./colours"; 4 | import Highlight, { defaultProps } from "prism-react-renderer"; 5 | import theme from "prism-react-renderer/themes/nightOwlLight"; 6 | import { textFromChildren } from "windups"; 7 | 8 | const rootStyle = css` 9 | font: 14px "Menlo", monospace; 10 | padding: 16px; 11 | border-radius: 5px; 12 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 13 | background: ${LIGHT_GREY}; 14 | margin-bottom: 8px; 15 | white-space: pre; 16 | line-height: 1.5; 17 | overflow: auto; 18 | `; 19 | 20 | const StaticCodeExample: React.FC = ({ children }) => { 21 | const text = textFromChildren(children); 22 | 23 | return ( 24 | 25 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 26 |
27 |           {tokens.map((line, i) => (
28 |             
29 | {line.map((token, key) => ( 30 | 31 | ))} 32 |
33 | ))} 34 |
35 | )} 36 |
37 | ); 38 | }; 39 | 40 | export default StaticCodeExample; 41 | -------------------------------------------------------------------------------- /src/Subheading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { css } from "linaria"; 3 | 4 | const rootStyle = css` 5 | font-family: Menlo, monospace; 6 | margin: 2em 0 1em 0; 7 | font-weight: normal; 8 | font-style: italic; 9 | font-size: 18px; 10 | `; 11 | 12 | const Subheading: React.FC = ({ children }) => { 13 | return

{children}

; 14 | }; 15 | 16 | export default Subheading; 17 | -------------------------------------------------------------------------------- /src/TextPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useContext, useEffect } from "react"; 2 | import { Linebreaker, WindupChildren, CharWrapper } from "windups"; 3 | import { css } from "linaria"; 4 | import useSize from "@rehooks/component-size"; 5 | import { CHAR_FONT_STYLE, StandardChar, CharContext } from "./Char"; 6 | import { DialogContext } from "./Dialog"; 7 | import RewindListener from "./RewindListener"; 8 | 9 | const rootStyle = css` 10 | line-height: 1.5em; 11 | `; 12 | 13 | const TextPanel: React.FC = ({ children }) => { 14 | const panelRef = useRef(null); 15 | const { width: panelWidth } = useSize(panelRef); 16 | const [isFinished, setIsFinished] = useState(false); 17 | const { isFinished: dialogIsFinished } = useContext(DialogContext); 18 | 19 | useEffect(() => { 20 | if (dialogIsFinished) { 21 | setIsFinished(true); 22 | } 23 | }, [dialogIsFinished, setIsFinished]); 24 | 25 | // TODO: code examples and normal examples are not skipped! 26 | 27 | return ( 28 | 29 |
30 | 31 | { 33 | setIsFinished(true); 34 | }} 35 | skipped={isFinished} 36 | > 37 | setIsFinished(false)} /> 38 | {children} 39 | 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default TextPanel; 47 | -------------------------------------------------------------------------------- /src/colours.ts: -------------------------------------------------------------------------------- 1 | export const GREEN = "#47bc76"; 2 | export const PINK = "#EFBBD1"; 3 | export const TEXT_PINK = "#ff92c0"; 4 | export const YELLOW = "#FFFCBE"; 5 | export const GREY = "#eeeeee"; 6 | export const LIGHT_GREY = "#f9f9f9"; 7 | -------------------------------------------------------------------------------- /src/content/Chat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { CharWrapper, WindupChildren, Linebreaker } from "windups"; 3 | import { css } from "linaria"; 4 | import { GREEN, PINK } from "../colours"; 5 | import useComponentSize from "@rehooks/component-size"; 6 | 7 | const chatChar = css` 8 | @keyframes enter { 9 | from { 10 | opacity: 0; 11 | transform: scale(0.1) rotate(-180deg) translateY(-100%); 12 | } 13 | to { 14 | opacity: 1; 15 | transform: scale(1) rotate(0deg) translateY(0); 16 | } 17 | } 18 | animation-name: enter; 19 | animation-duration: 150ms; 20 | display: inline-block; 21 | `; 22 | 23 | const SpeechBubbleChar: React.FC = ({ children }) => { 24 | return {children}; 25 | }; 26 | 27 | type SpeechBubbleProps = { 28 | text: string; 29 | onFinished?: () => void; 30 | }; 31 | 32 | const greenBubble = css` 33 | font-family: "Menlo", monospace; 34 | padding: 12px; 35 | color: white; 36 | border-radius: 3px; 37 | background-color: ${GREEN}; 38 | transform: skew(-5deg, -3deg); 39 | display: inline-block; 40 | white-space: pre-wrap; 41 | font-size: 24px; 42 | align-self: flex-start; 43 | box-shadow: 2px 2px 7px rgba(0, 0, 0, 0.05); 44 | `; 45 | 46 | export const SpeechBubbleA: React.FC = ({ 47 | text, 48 | onFinished, 49 | }) => { 50 | const ref = useRef(null); 51 | const { width } = useComponentSize(ref); 52 | 53 | return ( 54 |
55 | 56 | 57 |
58 | {text} 59 |
60 |
61 |
62 |
63 | ); 64 | }; 65 | 66 | const pinkBubble = css` 67 | font-family: "Menlo", monospace; 68 | padding: 12px; 69 | color: black; 70 | border-radius: 3px; 71 | background-color: ${PINK}; 72 | transform: skew(5deg, 3deg); 73 | display: inline-block; 74 | white-space: pre; 75 | font-size: 24px; 76 | align-self: flex-end; 77 | box-shadow: -2px 2px 7px rgba(0, 0, 0, 0.05); 78 | `; 79 | 80 | const rootStyle = css` 81 | display: flex; 82 | flex-direction: column; 83 | `; 84 | 85 | export const SpeechBubbleB: React.FC = ({ 86 | text, 87 | onFinished, 88 | }) => { 89 | const ref = useRef(null); 90 | const { width } = useComponentSize(ref); 91 | 92 | return ( 93 | 94 |
95 | 96 |
97 | {text} 98 |
99 |
100 |
101 |
102 | ); 103 | }; 104 | 105 | const chatRoot = css` 106 | display: flex; 107 | flex-direction: column; 108 | max-width: 50em; 109 | `; 110 | 111 | type ChatProps = { 112 | onFinished: () => void; 113 | }; 114 | 115 | const Chat: React.FC = ({ onFinished }) => { 116 | const [linesToShow, setLinesToShow] = React.useState(1); 117 | 118 | const setLines = (num: number) => { 119 | setTimeout(() => { 120 | setLinesToShow(num); 121 | }, 300); 122 | }; 123 | 124 | return ( 125 |
126 | setLines(2)} 129 | /> 130 | {linesToShow >= 2 && ( 131 | setLines(3)} 134 | /> 135 | )} 136 | {linesToShow >= 3 && ( 137 | setLines(4)} 140 | /> 141 | )} 142 | {linesToShow >= 4 && ( 143 | 147 | )} 148 |
149 | ); 150 | }; 151 | 152 | export default Chat; 153 | -------------------------------------------------------------------------------- /src/content/Guides.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GuidesHelp from "../guides/GuidesHelp"; 3 | import Install from "../guides/Install"; 4 | import HookIntro from "../guides/HookIntro"; 5 | import StyledText from "../guides/StyledText"; 6 | import WindupsWithAnything from "../guides/WindupsWithAnything"; 7 | import StylingCharacters from "../guides/StylingCharacters"; 8 | import Timing from "../guides/Timing"; 9 | import SkipRewind from "../guides/SkipRewind"; 10 | import Effects from "../guides/Effects"; 11 | import Linebreaking from "../guides/Linebreaking"; 12 | import SectionFocusContext from "../SectionFocusContext"; 13 | import Heading from "../Heading"; 14 | import ChangingValues from "../guides/ChangingValues"; 15 | import Accessibility from "../guides/Accessibility"; 16 | import LineBreakingWithStyle from "../guides/LinebreakingWithStyle"; 17 | 18 | const Guides: React.FC = () => { 19 | const [activeSectionID, setActiveSectionID] = React.useState( 20 | null 21 | ); 22 | 23 | return ( 24 | 30 | {"Guides"} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Guides; 49 | -------------------------------------------------------------------------------- /src/guides/Accessibility.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Section from "../Section"; 3 | import Dialog from "../Dialog"; 4 | import Frog from "../performers/Frog"; 5 | import { Emphasis } from "../Char"; 6 | import CodeExample from "../CodeExample"; 7 | 8 | const REDUCED_MOTION_EXAMPLE = `import { useWindupString } from "windups"; 9 | import { usePrefersReducedMotion } from "../a11y-hooks"; 10 | // Good example at https://joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion 11 | 12 | const SkipForReducedMotion = () => { 13 | const prefersReducedMotion = usePrefersReducedMotion(); 14 | const [text] = useWindupString("Respect user preferences!", { 15 | skipped: prefersReducedMotion, 16 | }); 17 | 18 | return
{text}
; 19 | };`; 20 | 21 | const SCREENREADER_EXAMPLE = `import { useWindupString } from "windups"; 22 | import { VisuallyHidden } from "@reach/visually-hidden"; 23 | 24 | const AccessibleWindupString = ({ text }) => { 25 | const [windupText] = useWindupString(text); 26 | 27 | return ( 28 | <> 29 | {text} 30 |
{windupText}
31 | 32 | ) 33 | } 34 | `; 35 | 36 | const SCREENREADER_CHILDREN_EXAMPLE = `import { WindupChildren, textFromChildren } from "windups"; 37 | import { VisuallyHidden } from "@reach/visually-hidden"; 38 | 39 | const AccessibleWindupChildren = ({ children }) => { 40 | const text = textFromChildren(children); 41 | 42 | return ( 43 | <> 44 | {text} 45 |
46 | 47 | {text} 48 | 49 |
50 | 51 | ) 52 | } 53 | `; 54 | 55 | const Accessibility: React.FC = () => { 56 | return ( 57 |
58 | 59 | 60 | { 61 | "It's hard to imagine... but not everyone is going to enjoy seeing a windup." 62 | } 63 | 64 | 65 | {"Maybe they get motion sick from all the animation."} 66 | 67 | 68 | { 69 | "Or maybe they use a screenreader, in which case they just want to hear the text all at once, not letter by letter!" 70 | } 71 | 72 | 73 | {"In these cases you'll want to"} 74 | {"disable"} 75 | {" or "} 76 | {"hide"} 77 | {" the windup effect."} 78 | 79 | 80 | { 81 | "To disable the effect, both useWindupString and WindupChildren accept a " 82 | } 83 | {"skipped"} 84 | {" prop, which will skip the effect as long as its value is true."} 85 | 86 | 87 | {"Here's how we could use that with users who prefer reduced motion:"} 88 | 89 | {REDUCED_MOTION_EXAMPLE} 90 | 91 | { 92 | "And now they can enjoy your epic eighty-hour amphibian legal drama the way " 93 | } 94 | {"they"} {" want to!"} 95 | 96 | 97 | { 98 | "And how about screenreaders? As your windup could be wrapped up in all kinds of markup to make it animate the way you want it, best way is to hide the windup altogether and expose the text to screenreaders only:" 99 | } 100 | 101 | {SCREENREADER_EXAMPLE} 102 | 103 | { 104 | "It's not always this easy to get the text you want people to see. Maybe your text is all mixed up in a big tree of children that you're passing to WindupChildren." 105 | } 106 | 107 | 108 | {"That's why we have a lil' "} 109 | {"textFromChildren"} 110 | { 111 | " function you can use to extract the text from a children parameter." 112 | } 113 | 114 | {SCREENREADER_CHILDREN_EXAMPLE} 115 | 116 | { 117 | "And with those techniques you can use windups without taking away anyone else's fun!" 118 | } 119 | 120 | 121 |
122 | ); 123 | }; 124 | 125 | export default Accessibility; 126 | -------------------------------------------------------------------------------- /src/guides/ChangingValues.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import Section from "../Section"; 3 | import Frog, { MadExpression, HappyExpression } from "../performers/Frog"; 4 | import CodeExample from "../CodeExample"; 5 | import { Emphasis } from "../Char"; 6 | import Dialog, { DialogChildContext } from "../Dialog"; 7 | import { WindupChildren, Pause } from "windups"; 8 | import Example from "../Example"; 9 | import { TEXT_PINK } from "../colours"; 10 | import SmashEffect from "../SmashEffect"; 11 | 12 | const TRICKY_EXAMPLE = `const TrickyWindup = () => { 13 | const [isPink, setIsPink] = useState(false); 14 | 15 | return ( 16 | <> 17 | 18 | 19 |
20 | {"Click the button to turn this pink! Or not?!"} 21 |
22 |
23 | 24 | ); 25 | }; 26 | `; 27 | 28 | const TrickyWindup = () => { 29 | const { proceed } = useContext(DialogChildContext); 30 | const [isPink, setIsPink] = useState(false); 31 | 32 | return ( 33 | <> 34 | 35 | 36 |
37 | {"Click the button to turn this pink! Or not?!"} 38 |
39 |
40 | 41 | ); 42 | }; 43 | 44 | const FIXED_EXAMPLE = `const FixedWindup = () => { 45 | const [isPink, setIsPink] = useState(false); 46 | 47 | return ( 48 | <> 49 | 50 | 51 |
55 | {"Click the button to turn this pink! Or not?!"} 56 |
57 |
58 | 59 | ); 60 | }; 61 | `; 62 | 63 | const FixedWindup = () => { 64 | const { proceed } = useContext(DialogChildContext); 65 | const [isPink, setIsPink] = useState(false); 66 | 67 | return ( 68 | <> 69 | 70 | 71 |
75 | {"Click the button to turn this pink! Or not?!"} 76 |
77 |
78 | 79 | ); 80 | }; 81 | 82 | const ChangingValues: React.FC = () => { 83 | return ( 84 |
85 | 86 | 87 | { 88 | "You may find yourself in a situation where the values you pass to useWindupString or WindupChildren are " 89 | } 90 | {"dynamic"} 91 | {"."} 92 | 93 | 94 | { 95 | "Let me be clear: changing the value of a windup while it's running will " 96 | } 97 | {"restart it from scratch."} 98 | 99 | 100 | {"At least... that's how it's meant to work."} 101 | 102 | 103 | { 104 | "See, with useWindupString it's easy to tell if the windup should restart: we just check if the string which was passed has changed." 105 | } 106 | 107 | 108 | {"But with WindupChildren this is a bit trickier..."} 109 | 110 | 111 | { 112 | "WindupChildren takes a children prop. But in React-land, technically speaking, the children prop " 113 | } 114 | {"changes on every render."} 115 | 116 | 117 | { 118 | "Which means that there's no way for us to tell if the value of the children prop has actually changed or not." 119 | } 120 | 121 | {"So we kinda had to get creative..."} 122 | 123 | { 124 | "Now, in most cases, if you change the structure or text of the children you pass to WindupChildren, it'll pick up on it and restart the windup." 125 | } 126 | 127 | 128 | {"But here's a situation it wouldn't be able to:"} 129 | 130 | {TRICKY_EXAMPLE} 131 | 132 | 133 | 134 | 135 | {"You will note that clicking the button does "} 136 | {"not"} 137 | {" turn the text pink."} 138 | 139 | 140 | 141 | {"So what's the deal?!"} 142 | 143 | 144 | {"WindupChildren is only able to compare the "} 145 | {"text content"} 146 | {" and "} 147 | {"shape"} 148 | {" of the children you pass it."} 149 | 150 | 151 | {"What it can't do is know if you changed the values of any "} 152 | {"components or their props"} 153 | {" inside that."} 154 | 155 | 156 | {"But it can see the "} 157 | {"keys"} 158 | {" used by those components."} 159 | 160 | {" That will let us fix the example from before!"} 161 | 162 | {FIXED_EXAMPLE} 163 | 164 | 165 | 166 | 167 | { 168 | "Well, hopefully this guide will save at least one person from totally losin' their marbles." 169 | } 170 | 171 | 172 |
173 | ); 174 | }; 175 | 176 | export default ChangingValues; 177 | -------------------------------------------------------------------------------- /src/guides/Effects.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { HappyExpression } from "../performers/Frog"; 5 | import SmashEffect from "../SmashEffect"; 6 | import { useWindupString, WindupChildren, OnChar } from "windups"; 7 | import Example from "../Example"; 8 | import CodeExample from "../CodeExample"; 9 | import { Emphasis } from "../Char"; 10 | import { SFXContext } from "../App"; 11 | 12 | const ON_FINISH_HOOK_EXAMPLE = `import { useWindupString } from "windups"; 13 | 14 | const OnFinishHookExample = () => { 15 | const [text] = useWindupString( 16 | "When this text finishes, I'll alert you. I'm sorry.", 17 | { 18 | onFinished: () => { 19 | alert("Finished!"); 20 | } 21 | } 22 | ); 23 | 24 | return
{text}
; 25 | } 26 | `; 27 | 28 | const OnFinishHookExample = () => { 29 | const { proceed } = useContext(DialogChildContext); 30 | 31 | const [text] = useWindupString( 32 | "When this text finishes, I'll alert you. I'm sorry.", 33 | { 34 | onFinished: () => { 35 | alert("Finished!"); 36 | proceed(); 37 | }, 38 | } 39 | ); 40 | 41 | return
{text}
; 42 | }; 43 | 44 | const EFFECT_EXAMPLE = `import { useWindupString } from "windups"; 45 | import { smash } from 'gratuitous-fx'; 46 | 47 | const EffectExample = () => { 48 | return ( 49 | 50 | {"I carefully steadied my aim... and "} 51 | 52 | {"struck! The carrot was cut clean in half."} 53 | 54 | ); 55 | }; 56 | `; 57 | 58 | const EffectExample = () => { 59 | const { proceed } = useContext(DialogChildContext); 60 | 61 | return ( 62 | 63 | {"I carefully steadied my aim... and "} 64 | 65 | {"struck! The carrot was cut clean in half."} 66 | 67 | ); 68 | }; 69 | 70 | const ON_CHAR_EXAMPLE = `import { WindupChildren } from "windups"; 71 | 72 | const ContrivedOnCharExample = () => { 73 | const [count, setCount] = React.useState(0); 74 | const [text] = useWindupString( 75 | "You'll probably want to do something more interesting than this!", 76 | { 77 | onChar: () => setCount(prev => prev + 1), 78 | } 79 | ); 80 | 81 | return ( 82 |
83 |
{text}
84 |
{"Characters printed: "}{count}
85 |
86 | ); 87 | }; 88 | `; 89 | 90 | const ContrivedOnCharExample = () => { 91 | const { proceed } = useContext(DialogChildContext); 92 | const [count, setCount] = React.useState(0); 93 | const [text] = useWindupString( 94 | "You'll probably want to do something more interesting than this!", 95 | { 96 | onChar: () => setCount((prev) => prev + 1), 97 | onFinished: proceed, 98 | } 99 | ); 100 | 101 | return ( 102 |
103 |
{text}
104 |
{`Characters printed: ${count}`}
105 |
106 | ); 107 | }; 108 | 109 | const ON_CHAR_CHILDREN_EXAMPLE = `import { WindupChildren, OnChar } from "windups"; 110 | import { smash } from "gratuitous-fx"; 111 | 112 | const OnCharChildrenExample = () => { 113 | const { smash } = useContext(SFXContext); 114 | 115 | return ( 116 | 117 | {"Hey! Use the brakes! "} 118 | {"Noooo!"} 119 | 120 | ); 121 | }; 122 | `; 123 | 124 | const OnCharChildrenExample = () => { 125 | const { smash } = useContext(SFXContext); 126 | const { proceed } = useContext(DialogChildContext); 127 | 128 | return ( 129 | 130 | {"Hey! Use the brakes! "} 131 | {"Noooo!"} 132 | 133 | ); 134 | }; 135 | 136 | const Effects = () => { 137 | return ( 138 |
139 | 140 | 141 | { 142 | "Addin' the element of time to yer' text means there'll be plenty of occasions you'll want something to happen at just the right moment." 143 | } 144 | 145 | {"Maybe you'll want to save a user's reading progress."} 146 | 147 | {"Or B"} 148 | 149 | 150 | {"AM! Add a special effect."} 151 | 152 | 153 | { 154 | "One of the most common cases is you'll want to do something when a windup " 155 | } 156 | {"finishes"} 157 | {"."} 158 | 159 | {ON_FINISH_HOOK_EXAMPLE} 160 | 161 | 162 | 163 | 164 | {"WindupChildren has something similar: an"}{" "} 165 | {"onFinished"} {"prop."} 166 | 167 | 168 | { 169 | "Making something happen at the end is just one common use case. Maybe you want to call a function at a precise moment?" 170 | } 171 | 172 | {EFFECT_EXAMPLE} 173 | 174 | 175 | 176 | 177 | {"Using WindupChildren with the "} 178 | {"Effect"} 179 | {" component's by far the easiest way to do something like that."} 180 | 181 | 182 | { 183 | "There's another common pattern I see... and that's when you want to fire a function every time a character's added." 184 | } 185 | 186 | {ON_CHAR_EXAMPLE} 187 | 188 | 189 | 190 | 191 | { 192 | "And if you need different onChar callbacks for different parts of your text, WindupChildren supports an " 193 | } 194 | {"OnChar"} 195 | {" component."} 196 | 197 | {ON_CHAR_CHILDREN_EXAMPLE} 198 | 199 | 200 | 201 | {"..."} 202 | {"That's it."} 203 | 204 | {"Ain't you gettin' tired of all these code examples yet?"} 205 | 206 | 207 |
208 | ); 209 | }; 210 | 211 | export default Effects; 212 | -------------------------------------------------------------------------------- /src/guides/GuidesHelp.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Section from "../Section"; 3 | import Dialog from "../Dialog"; 4 | import Frog from "../performers/Frog"; 5 | import { Pace, Pause } from "windups"; 6 | import SmashEffect from "../SmashEffect"; 7 | import { Emphasis, Dropped } from "../Char"; 8 | 9 | function getRandomArbitrary(min: number, max: number) { 10 | return Math.random() * (max - min) + min; 11 | } 12 | 13 | const GuidesHelp: React.FC = () => { 14 | return ( 15 |
16 | 17 | 18 | { 19 | "If you're going to read a few of these guides, it's worth learning a few little tricks." 20 | } 21 | 22 | 23 | {"Like you can continue to the next bit of dialogue by pressing "} 24 | {"return"} 25 | {" or the "} 26 | {"right arrow key."} 27 | {" Life changing, right?"} 28 | 29 | 30 | {"Give it a go!"} 31 | 32 | 33 | {"Ow! Did you have to press so hard?!"} 34 | 35 | 36 | 37 | { 38 | "Oh, and if you're the impatient type, pressing either of these keys will " 39 | } 40 | {"fast forward"} 41 | {" what I'm sayin'."} 42 | 43 | 44 | getRandomArbitrary(150, 200)}> 45 | {"But why would you want to do that?"} 46 | 47 | 48 | 49 | 50 | {"All right, I was just havin' some fun."} 51 | 52 | 53 | 54 | { 55 | "One last thing. If you fancy seeing a bit of dialog play out again — I dunno, some people say they like my voice's timbre — you can press the " 56 | } 57 | {"left arrow key"} 58 | {" to restart it."} 59 | 60 | {"Now you know the secrets. Happy readin'."} 61 | 62 |
63 | ); 64 | }; 65 | 66 | export default GuidesHelp; 67 | -------------------------------------------------------------------------------- /src/guides/HookIntro.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { SmugExpression } from "../performers/Frog"; 5 | import SmashEffect from "../SmashEffect"; 6 | import CodeExample from "../CodeExample"; 7 | import Example from "../Example"; 8 | import { useWindupString } from "windups"; 9 | import { Emphasis } from "../Char"; 10 | 11 | const HOOK_EXAMPLE = `import React from "react"; 12 | import { useWindupString } from "windups"; 13 | 14 | // Make a new component 15 | const StringyWindup = () => { 16 | const [text] = useWindupString("Hello world!"); 17 | 18 | return
{text}
; 19 | };`; 20 | 21 | const StringyWindup = () => { 22 | const { proceed } = useContext(DialogChildContext); 23 | 24 | const [text] = useWindupString("Hello world!", { 25 | onFinished: proceed, 26 | }); 27 | 28 | return
{text}
; 29 | }; 30 | 31 | const HookIntro: React.FC = () => { 32 | return ( 33 |
34 | 35 | 36 | {"Okay, enough talking. Let's "} 37 | 38 | 39 | {"windup!"} 40 | 41 | 42 | { 43 | "If all you want to do is add the windup effect to a bit of text, I got just the thing. It's one of them new " 44 | } 45 | {"React Hooks "} 46 | {"you see city folk using."} 47 | 48 | {"Hang on, lemme type this out..."} 49 | {HOOK_EXAMPLE} 50 | 51 | 52 | 53 | 54 | { 55 | "That's all. Give a string, get back a string. But now the string's all windup-like." 56 | } 57 | 58 | 59 | { 60 | "The text variable we got back from the hook will just be the letter 'H' at first. Then 'He'. Then 'Hel'. And so on. You get it?" 61 | } 62 | 63 | 64 | { 65 | "For a lot of people, that's all they need. But maybe you're different. Maybe you're sayin' \"I've got needs! Different parts of my text need their own styles!\"" 66 | } 67 | 68 | 69 | { 70 | "If that's you, relax. I got you covered. Check out the guide for WindupChildren." 71 | } 72 | 73 | 74 |
75 | ); 76 | }; 77 | 78 | export default HookIntro; 79 | -------------------------------------------------------------------------------- /src/guides/Install.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Section from "../Section"; 3 | import Dialog from "../Dialog"; 4 | import Frog, { ShameExpression, HappyExpression } from "../performers/Frog"; 5 | import CodeExample from "../CodeExample"; 6 | import { Emphasis } from "../Char"; 7 | 8 | const Install: React.FC = () => { 9 | return ( 10 |
11 | 12 | {"npm install windups"} 13 | 14 | { 15 | "Wait just one minute! Are you saying we expect everyone to know what that means?" 16 | } 17 | 18 | 19 | {"So... in case three random words doesn't mean anything to you..."} 20 | 21 | 22 | {"To use windups, you have to add it as a "} 23 | {"dependency"} 24 | {" to your project first."} 25 | 26 | 27 | { 28 | "Navigate to the directory containing your project in your command line, and type this..." 29 | } 30 | 31 | {"npm install windups"} 32 | 33 | { 34 | "And hit return! That should download windups' code to your computer so that you can actually use it." 35 | } 36 | 37 | 38 | { 39 | "If that didn't work, or if you don't know what I'm talking about, well..." 40 | } 41 | 42 | {"I'd start by searching online..."} 43 | 44 | 45 | {"If you're just starting with programming,"} 46 | 47 | {" that's a skill you'll be usin' a lot!"} 48 | 49 | 50 |
51 | ); 52 | }; 53 | 54 | export default Install; 55 | -------------------------------------------------------------------------------- /src/guides/Linebreaking.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { ShameExpression } from "../performers/Frog"; 5 | import { useWindupString, WindupChildren, Linebreaker, Pace } from "windups"; 6 | import Example from "../Example"; 7 | import CodeExample from "../CodeExample"; 8 | import SmashEffect from "../SmashEffect"; 9 | import { Emphasis } from "../Char"; 10 | import useSize from "@rehooks/component-size"; 11 | 12 | const BadlyBreakingLine = () => { 13 | const { proceed } = useContext(DialogChildContext); 14 | const [text] = useWindupString("Is line-breaking necessary?", { 15 | pace: () => 200, 16 | onFinished: proceed, 17 | }); 18 | 19 | return
{text}
; 20 | }; 21 | 22 | const FULL_EXAMPLE = `import { Linebreaker, WindupChildren } from "windups"; 23 | 24 | const GoodBreakingLine = () => { 25 | return ( 26 | 27 |
28 | 29 | {"Is line-breaking necessary?"} 30 | 31 |
32 |
33 | ); 34 | } 35 | `; 36 | 37 | const GoodBreakingLine = () => { 38 | const divRef = React.useRef(null); 39 | const { width } = useSize(divRef); 40 | const { proceed } = useContext(DialogChildContext); 41 | 42 | return ( 43 | 44 |
45 | 46 | {"Is line-breaking necessary?"} 47 | 48 |
49 |
50 | ); 51 | }; 52 | 53 | const STEP_ONE = ` 54 | 55 | {"Is line-breaking necessary?"} 56 | 57 | 58 | `; 59 | 60 | const STEP_TWO = ` 61 |
62 | 63 | {"Is line-breaking necessary?"} 64 | 65 |
66 |
67 | `; 68 | 69 | const Linebreaking = () => { 70 | return ( 71 |
72 | 73 | 74 | { 75 | "If you use a lot of windups, you're going to bump into this problem sooner or later." 76 | } 77 | 78 | {"Brace 'yerself."} 79 | 80 | 81 | 82 | 83 | { 84 | "You saw it, right? How the words were jumping from line to line like eyes ain't a thing?" 85 | } 86 | 87 | {"I know: it's disgusting."} 88 | 89 | {"But what are we supposed to do?"} 90 | 91 | { 92 | " How're you supposed to know when to break a line when you don't have the whole word yet?!" 93 | } 94 | 95 | 96 | { 97 | "So we got this new stuff from the frogs up at the advanced windups lab." 98 | } 99 | 100 | 101 | {"They call it the "} 102 | {"Linebreaker"} 103 | {" component, and sent this example."} 104 | 105 | {FULL_EXAMPLE} 106 | 107 | 108 | 109 | 110 | 111 | {"See that?! Now that's how you break a line!"} 112 | 113 | 114 | { 115 | "But it's a lot to take in, I know. We'll go through it step by step." 116 | } 117 | 118 | 119 | {"Firstly, we wrap everything inside the Linebreaker component."} 120 | 121 | {STEP_ONE} 122 | {"For props we give it a font style, and a width."} 123 | {STEP_TWO} 124 | 125 | { 126 | "It then looks for text inside of the children that's been provided to it." 127 | } 128 | 129 | 130 | { 131 | "And then takes these things... the available width, the font style, and the text, and puts it all together to figure out where new lines should start before it's ever laid out by the browser." 132 | } 133 | 134 | {"Now, 'course this approach has caveats."} 135 | 136 | { 137 | "Firstly you gotta know the style of your text, and the width available for it to fit in beforehand. Seems reasonable enough." 138 | } 139 | 140 | {"All the text has to be the same style for it to work."} 141 | 142 | { 143 | "And if you render non-text stuff that adds to the horizontal content of the line, it's all gonna get out of shape. " 144 | } 145 | 146 | 147 | { 148 | "Finally, the placement of this component really matters. It's gotta be " 149 | } 150 | {"outside the WindupChildren component"} 151 | { 152 | ". Otherwise WindupChildren will feed incomplete text into the Linebreaker component and it'll all get weird." 153 | } 154 | 155 | 156 | { 157 | "Which is coincidentally the same reason that it doesn't work with useWindupString. If you want to do that, you can use the " 158 | } 159 | {"break-styled-lines"} 160 | {" package that powers Linebreaker."} 161 | 162 | 163 |
164 | ); 165 | }; 166 | 167 | export default Linebreaking; 168 | -------------------------------------------------------------------------------- /src/guides/LinebreakingWithStyle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { 5 | ShameExpression, 6 | MadExpression, 7 | HappyExpression, 8 | } from "../performers/Frog"; 9 | import { WindupChildren, Linebreaker, Pace, Pause, StyledText } from "windups"; 10 | import Example from "../Example"; 11 | import CodeExample from "../CodeExample"; 12 | import SmashEffect from "../SmashEffect"; 13 | import { Emphasis, Thinking } from "../Char"; 14 | import useSize from "@rehooks/component-size"; 15 | 16 | const BROKEN_EXAMPLE = ` 17 | 18 | {"Let's talk about the "} 19 | {"elephant"} 20 | {" in the room..."} 21 | 22 | `; 23 | 24 | const FIXED_EXAMPLE = ` 25 | 26 | {"Let's talk about the "} 27 | 28 | {"elephant"} 29 | 30 | {" in the room..."} 31 | 32 | `; 33 | 34 | const VanillaBreakingLine = () => { 35 | const divRef = React.useRef(null); 36 | const { width } = useSize(divRef); 37 | const { proceed } = useContext(DialogChildContext); 38 | 39 | return ( 40 | 41 |
42 | 43 | {"Let's talk about the elephant in the room..."} 44 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | const BadBreakingLine = () => { 51 | const divRef = React.useRef(null); 52 | const { width } = useSize(divRef); 53 | const { proceed } = useContext(DialogChildContext); 54 | 55 | return ( 56 | 57 |
58 | 59 | 60 | {"Let's talk about the "} 61 | {"elephant"} 62 | {" in the room..."} 63 | 64 | 65 |
66 |
67 | ); 68 | }; 69 | 70 | const StyledBreakingLine = () => { 71 | const divRef = React.useRef(null); 72 | const { width } = useSize(divRef); 73 | const { proceed } = useContext(DialogChildContext); 74 | 75 | return ( 76 | 77 |
78 | 79 | 80 | {"Let's talk about the "} 81 | 82 | {"elephant"} 83 | 84 | {" in the room..."} 85 | 86 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | const SeveralHoursLater = () => { 93 | const { proceed } = useContext(DialogChildContext); 94 | const divRef = React.useRef(null); 95 | const { width } = useSize(divRef); 96 | 97 | return ( 98 | 99 |
108 | 109 | {"SEVERAL HOURS LATER..."} 110 | 111 | 112 |
113 |
114 | ); 115 | }; 116 | 117 | const LinebreakingWithStyle = () => { 118 | return ( 119 |
120 | 121 | {"I've got this headline I'm working on for someone."} 122 | 123 | 124 | 125 | 126 | { 127 | "That's when I had this idea! How wild would it be if the word elephant was big —" 128 | } 129 | 130 | 131 | {"like an elephant?!"} 132 | 133 | 134 | 135 | 136 | 137 | {"Sometimes it hurts thinking how smart I a —"} 138 | 139 | 140 | {"woah what?!"} 141 | 142 | 143 | {"What's with that screwy linebreaking?"} 144 | 145 | {"Let's take a look at the code..."} 146 | {BROKEN_EXAMPLE} 147 | {"Oh... right."} 148 | 149 | { 150 | "On the first line we tell that the fontStyle will be " 151 | } 152 | {"36px"} 153 | 154 | {"."} 155 | 156 | {"But then we go ahead and style the word 'elephant' as "} 157 | {"72px"} 158 | {"!"} 159 | 160 | 161 | { 162 | "If only there were some way to get in on my amazing joke..." 163 | } 164 | 165 | 166 | 167 | 168 | {"I've got it!"} 169 | 170 | 171 | 172 | 173 | 174 | { 175 | "See that? Words going where you expect 'em to! Nothing can stop this frog!" 176 | } 177 | 178 | 179 | 180 | 181 | {"(Totally worth it for that elephant joke...)"} 182 | 183 | 184 | 185 | 186 | { 187 | 'What we needed was a way to tell "the style is different here!"' 188 | } 189 | 190 | 191 | {"So we cooked up this fancy "} 192 | {""} 193 | {" component that does just that! Check it out!"} 194 | 195 | {FIXED_EXAMPLE} 196 | 197 | {"Now you can have differently styled text in the same run "} 198 | {"without screwing up yer' linebreaking!"} 199 | 200 | 201 | { 202 | "But... uh, I hope yer' needs won't get much more complicated than that." 203 | } 204 | 205 | 206 |
207 | ); 208 | }; 209 | 210 | export default LinebreakingWithStyle; 211 | -------------------------------------------------------------------------------- /src/guides/SkipRewind.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { ShockExpression } from "../performers/Frog"; 5 | import SmashEffect from "../SmashEffect"; 6 | import { useWindupString, WindupChildren, useSkip, Pace } from "windups"; 7 | import Example from "../Example"; 8 | import CodeExample from "../CodeExample"; 9 | import { Emphasis } from "../Char"; 10 | 11 | const SKIP_HOOK_EXAMPLE = `import { useWindupString } from "windups"; 12 | 13 | const SkipHookExample = () => { 14 | const [text, { skip }] = useWindupString( 15 | "A fly? A fly! Why, oh why, would one cry for a fly?", 16 | { 17 | pace: () => 400, 18 | onFinished: proceed, 19 | } 20 | ); 21 | 22 | return ( 23 |
24 |
{text}
25 | 26 |
27 | ); 28 | }`; 29 | 30 | const SkipHookExample = () => { 31 | const { proceed } = useContext(DialogChildContext); 32 | 33 | const [text, { skip }] = useWindupString( 34 | "A fly? A fly! Why, oh why, would one cry for a fly?", 35 | { 36 | pace: () => 400, 37 | onFinished: proceed 38 | } 39 | ); 40 | 41 | return ( 42 |
43 |
{text}
44 | 45 |
46 | ); 47 | }; 48 | 49 | const SKIP_CHILDREN_EXAMPLE = `import { WindupChildren, useSkip } from "windups"; 50 | 51 | const SkipButton = () => { 52 | const skip = useSkip(); 53 | 54 | return ; 55 | }; 56 | 57 | const SkippableWindupChildren = () => { 58 | const { proceed } = useContext(DialogChildContext); 59 | 60 | return ( 61 | 62 | 63 |
64 | 65 | {"Why, if one would lie on a fly it would assuredly die."} 66 | 67 |
68 |
69 | ); 70 | };`; 71 | 72 | const SkipButton = () => { 73 | const skip = useSkip(); 74 | 75 | return ; 76 | }; 77 | 78 | const SkippableWindupChildren = () => { 79 | return ( 80 | 81 | 82 |
83 | 84 | {"Why, if one would lie on a fly it would assuredly die."} 85 | 86 |
87 |
88 | ); 89 | }; 90 | 91 | const SkipAndRewind = () => { 92 | return ( 93 |
94 | 95 | 96 | { 97 | "If you're using windups with lots of text, you should consider the feelin's of who's reading them." 98 | } 99 | 100 | 101 | { 102 | "Much as I hate to say it, sometimes people don't want to sit through your windup song and dance. They just want to read the text." 103 | } 104 | 105 | 106 | {"And y'know what? I respect that. That's why there's a way to "} 107 | {"skip"} 108 | {" a windup."} 109 | 110 | {SKIP_HOOK_EXAMPLE} 111 | 112 | 113 | 114 | 115 | { 116 | "I wrote that. I feel like the slow delivery gives it time to breathe, but I'm not gonna force it on anyone." 117 | } 118 | 119 | 120 | { 121 | "Anyway. Just grab the skip callback from the hook and you're good to go." 122 | } 123 | 124 | 125 | { 126 | "But how about WindupChildren? Where's the skip callback going to come from in there?" 127 | } 128 | 129 | {SKIP_CHILDREN_EXAMPLE} 130 | 131 | 132 | 133 | 134 | { 135 | "You mighta' noticed that in the WindupChildren example, the SkipButton's rendered at the top." 136 | } 137 | 138 | 139 | { 140 | "There's no easy way around this... anything using the useSkip hook needs to be rendered within WindupChildren to work. But that also makes it part of the windup effect!" 141 | } 142 | 143 | 144 | { 145 | "So if that SkipButton was put below the text, it wouldn't appear until all the text had printed out... which kinda misses the point, don't it?" 146 | } 147 | 148 | 149 | { 150 | "How to get around that? Well, you could put the SkipButton first thing in the Windup but make it " 151 | } 152 | {"look like it's below the text"} 153 | {" using CSS."} 154 | 155 | 156 | { 157 | "Or you could do what we did here and just have an invisible key listening component rendered at the beginning!" 158 | } 159 | 160 | 161 | {"Well, that just about cove-"} 162 | 163 | 164 | {"Hold it!"} 165 | 166 | 167 | { 168 | "I can't believe I nearly forgot. Not all people are in a hustle-bustle to skip through windups." 169 | } 170 | 171 | {"Maybe some people want to savour it."} 172 | {"Experience it over and over."} 173 | 174 | {"In that case, there's a way to rewind the windup effect!"} 175 | 176 | 177 | { 178 | "It pretty much works the same way for the hook and WindupChildren. Check the docs." 179 | } 180 | 181 | 182 | { 183 | "What? You were expecting more poems 'bout flies? I ain't got all day, kid." 184 | } 185 | 186 | 187 |
188 | ); 189 | }; 190 | 191 | export default SkipAndRewind; 192 | -------------------------------------------------------------------------------- /src/guides/StyledText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { HappyExpression } from "../performers/Frog"; 5 | import SmashEffect from "../SmashEffect"; 6 | import CodeExample from "../CodeExample"; 7 | import { TEXT_PINK } from "../colours"; 8 | import { useWindupString, WindupChildren } from "windups"; 9 | import Example from "../Example"; 10 | import { Emphasis } from "../Char"; 11 | 12 | const UNSTYLED_EXAMPLE = `import React from "react"; 13 | import { useWindupString } from "windups"; 14 | 15 | const DressCodeWarning = () => { 16 | const { proceed } = useContext(DialogChildContext); 17 | const [text] = useWindupString( 18 | "This club admits only those wearing bright pink hats." 19 | ); 20 | 21 | return
{text}
; 22 | };`; 23 | 24 | const UnstyledExample: React.FC = () => { 25 | const { proceed } = useContext(DialogChildContext); 26 | const [text] = useWindupString( 27 | "This club admits only those wearing bright pink hats.", 28 | { 29 | onFinished: proceed 30 | } 31 | ); 32 | 33 | return
{text}
; 34 | }; 35 | 36 | const STYLED_EXAMPLE = `import React from "react"; 37 | import { WindupChildren } from "windups"; 38 | 39 | const StyledExample = () => { 40 | return ( 41 | 42 | {"This club admits only those wearing "} 43 | {"bright pink"} 44 | {" hats."} 45 | 46 | ); 47 | };`; 48 | 49 | const StyledExample = () => { 50 | const { proceed } = useContext(DialogChildContext); 51 | 52 | return ( 53 | 54 | {"This club admits only those wearing "} 55 | {"bright pink"} 56 | {" hats."} 57 | 58 | ); 59 | }; 60 | 61 | const GuideName = () => { 62 | return ( 63 |
64 | 65 | {"Take a look at this."} 66 | {UNSTYLED_EXAMPLE} 67 | 68 | 69 | 70 | 71 | { 72 | "Now, I don't know what it is, but a lot of people, when they see this, they get an idea in their heads." 73 | } 74 | 75 | 76 | { 77 | "What they want more than anything is for the words 'bright pink' to be, well, " 78 | } 79 | {"bright pink"} 80 | {"."} 81 | 82 | 83 | { 84 | "And I tell 'em, you try and do that with useWindupString, and yer' gonna tie yerself up in knots!" 85 | } 86 | 87 | {" Yar har har har!"} 88 | 89 | 90 | {"Lucky for them, I got something else cooked up for that."} 91 | 92 | {STYLED_EXAMPLE} 93 | 94 | 95 | 96 | 97 | {"With this WindupChildren component, it's easy to "} 98 | {"style different segments of text"} 99 | { 100 | ". You just put what you want in there, and WindupChildren will make a windup out of it." 101 | } 102 | 103 | 104 | { 105 | "If yer' of a devious disposition you may be thinking, \"if I can put anything in there, what's stopping me from putting an " 106 | } 107 | {"image"} 108 | {" in there? Or a "} 109 | {"button"} 110 | {'?"'} 111 | 112 | 113 | 114 | {"Nothing, that's what! So you gotta use some common sense."} 115 | 116 | 117 | {"But if yer' interested in how to use stuff "} 118 | {"other than text"} 119 | {" in your windups, check out the next guide."} 120 | 121 | 122 |
123 | ); 124 | }; 125 | 126 | export default GuideName; 127 | -------------------------------------------------------------------------------- /src/guides/StylingCharacters.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { ShockExpression, ShameExpression } from "../performers/Frog"; 5 | import CodeExample from "../CodeExample"; 6 | import { useWindupString, CharWrapper, WindupChildren, Pace } from "windups"; 7 | import SmashEffect from "../SmashEffect"; 8 | import { Emphasis } from "../Char"; 9 | import { css } from "linaria"; 10 | import Example from "../Example"; 11 | 12 | const BORING_EXAMPLE = `import React from "react"; 13 | import { useWindupString } from "windups"; 14 | 15 | const VanillaWindup = () => { 16 | const [text] = useWindupString("Baked Beans On Toast"); 17 | 18 | return
{text}
; 19 | };`; 20 | 21 | const STRING_EXAMPLE = `import React from "react"; 22 | import { useWindupString, CharWrapper } from "windups"; 23 | import { css } from "unnamed-styling-library"; 24 | 25 | // Our animation CSS. A slow fade in. 26 | const fadeInAnimationStyle = css\` 27 | @keyframes fadeIn { 28 | from { 29 | opacity: 0; 30 | } 31 | to { 32 | opacity: 1; 33 | } 34 | } 35 | animation-name: fadeIn; 36 | animation-duration: 3s; 37 | animation-iteration-count: 1; 38 | \`; 39 | 40 | // A component to wrap around each character. 41 | const SpookyChar = ({children}) => { 42 | return {children} 43 | } 44 | 45 | // Our windup 46 | const GhostlyWindup = () => { 47 | const [text] = useWindupString("Baked Beans On Toast"); 48 | 49 | return
{text}
; 50 | };`; 51 | 52 | const WITHOUT_CHARWRAPPER_EXAMPLE = `return ( 53 |
54 | {text.split("").map(char => ( 55 | {char} 56 | ))} 57 |
58 | );`; 59 | 60 | const WINDUP_CHILDREN_EXAMPLE = `import React from "react"; 61 | import { WindupChildren, CharWrapper } from "windups"; 62 | import { SpookyChar } from "./that-last-example"; 63 | 64 | const SpookyEmphasisedWindup = () => { 65 | return ( 66 | 67 | 68 | {"Baked "} 69 | {"beans"} 70 | {" on toast"} 71 | 72 | 73 | ) 74 | } 75 | `; 76 | 77 | // Our animation CSS. A simple fade. 78 | const fadeInAnimationStyle = css` 79 | @keyframes fadeIn { 80 | from { 81 | opacity: 0; 82 | } 83 | to { 84 | opacity: 1; 85 | } 86 | } 87 | animation-name: fadeIn; 88 | animation-duration: 3s; 89 | animation-iteration-count: 1; 90 | `; 91 | 92 | const VanillaWindup = () => { 93 | const { proceed } = useContext(DialogChildContext); 94 | const [text] = useWindupString("Baked Beans On Toast", { 95 | onFinished: proceed 96 | }); 97 | 98 | return
{text}
; 99 | }; 100 | 101 | const SpookyChar: React.FC = ({ children }) => { 102 | return {children}; 103 | }; 104 | 105 | const GhostlyWindup = () => { 106 | const { proceed } = useContext(DialogChildContext); 107 | 108 | const [text] = useWindupString("Baked Beans On Toast", { 109 | onFinished: proceed, 110 | pace: () => 200 111 | }); 112 | 113 | return ( 114 |
115 | {text} 116 |
117 | ); 118 | }; 119 | 120 | const SpookyEmphasisedWindup = () => { 121 | const { proceed } = useContext(DialogChildContext); 122 | 123 | return ( 124 | 125 | 126 | 127 | {"Baked "} 128 | {"beans"} 129 | {" on toast"} 130 | 131 | 132 | 133 | ); 134 | }; 135 | 136 | const StylingCharacters: React.FC = () => { 137 | return ( 138 |
142 | 143 | {"Tell me if you see something wrong here:"} 144 | {BORING_EXAMPLE} 145 | 146 | 147 | 148 | {"No? Still don't see it? Need a hint?"} 149 | 150 | {"It's "} 151 | 152 | {"boring!"} 153 | 154 | 155 | { 156 | "Sure, in the right spot a windup's better than no windup. But to really shine, a windup needs... " 157 | } 158 | {"panache."} 159 | 160 | 161 | {"Flair."} 162 | 163 | 164 | 165 | {"Styyyyyyle."} 166 | 167 | 168 | 169 | 170 | {"Ya get me?!"} 171 | 172 | 173 | {"What I'm saying is it looks "} 174 | {"real cool"} 175 | {" when you animate the characters "} 176 | {"individually as they come in."} 177 | 178 | {"Let's try it out first with useWindupString."} 179 | {STRING_EXAMPLE} 180 | 181 | 182 | 183 | 184 | {"See, a little animation turned what "} 185 | {"was a menu item"} 186 | {" into a "} 187 | 188 | {"chilling final warning"} 189 | {"."} 190 | 191 | 192 | {"Anyway, about that "} 193 | {"CharWrapper"} 194 | {" component..."} 195 | 196 | 197 | { 198 | "With useWindupString, it's just a convenience. You could also do this:" 199 | } 200 | 201 | {WITHOUT_CHARWRAPPER_EXAMPLE} 202 | {"But why would you?"} 203 | 204 | { 205 | "Where you'll really want CharWrapper is when you use WindupChildren." 206 | } 207 | 208 | {WINDUP_CHILDREN_EXAMPLE} 209 | 210 | 211 | 212 | 213 | { 214 | "Without CharWrapper, you'd have to wrap each character individually! What a bore that'd be!" 215 | } 216 | 217 | 218 | { 219 | "Anyway. I got one last tip for you, if you're planning to animate characters individually using " 220 | } 221 | {"CSS transforms"} 222 | {"."} 223 | 224 | 225 | {"CSS transforms "} 226 | {"don't work on inline elements"} 227 | 228 | { 229 | "! So you'll probably have to render your characters as inline-block. Make sense?" 230 | } 231 | 232 | {"All right. That's enough about that."} 233 | 234 |
235 | ); 236 | }; 237 | 238 | export default StylingCharacters; 239 | -------------------------------------------------------------------------------- /src/guides/Timing.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { ShameExpression, HappyExpression } from "../performers/Frog"; 5 | import CodeExample from "../CodeExample"; 6 | import { Pause, useWindupString, WindupChildren, Pace } from "windups"; 7 | import Example from "../Example"; 8 | import { Emphasis } from "../Char"; 9 | 10 | const PACE_HOOK_EXAMPLE = `import React from "react"; 11 | import { useWindupString } from "windups"; 12 | 13 | const ThinkingHard = () => { 14 | const [text] = useWindupString("I'm thinking really hard.", { 15 | pace: (char) => (char === " " ? 600 : 40), 16 | }); 17 | 18 | return
{text}
; 19 | };`; 20 | 21 | const PACE_COMPONENT_EXAMPLE = `import React from "react"; 22 | import { Pace, WindupChildren } from "windups"; 23 | 24 | const SassyThinkingHard = () => { 25 | return ( 26 | 27 | {"Didn't you hear me the first time? "} 28 | (char === " " ? 600 : 40)}> 29 | {"I'm thinking really hard."} 30 | 31 | 32 | ); 33 | };`; 34 | 35 | const RobotExample = () => { 36 | const { proceed } = useContext(DialogChildContext); 37 | const [text] = useWindupString( 38 | "Hello friend. I am definitely a living thing and not a computer. Ha ha ha", 39 | { 40 | pace: () => 70, 41 | onFinished: proceed 42 | } 43 | ); 44 | 45 | return
{text}
; 46 | }; 47 | 48 | const BetterExample = () => { 49 | const { proceed } = useContext(DialogChildContext); 50 | const [text] = useWindupString( 51 | "Hello friend. I am definitely a living thing and not a computer. Ha ha ha", 52 | { 53 | onFinished: proceed 54 | } 55 | ); 56 | 57 | return
{text}
; 58 | }; 59 | 60 | const PaceStringExample = () => { 61 | const { proceed } = useContext(DialogChildContext); 62 | 63 | const [text] = useWindupString("I'm thinking really hard.", { 64 | onFinished: proceed, 65 | pace: char => (char === " " ? 600 : 40) 66 | }); 67 | 68 | return
{text}
; 69 | }; 70 | 71 | const PaceComponentExample = () => { 72 | const { proceed } = useContext(DialogChildContext); 73 | 74 | return ( 75 | 76 | {"Didn't you hear me the first time? "} 77 | (char === " " ? 600 : 40)}> 78 | {"I'm thinking really hard."} 79 | 80 | 81 | ); 82 | }; 83 | 84 | const PAUSED_CODE_EXAMPLE = `import { WindupChildren, Pause } from "windups"; 85 | 86 | const PausedExample = () => { 87 | return ( 88 | 89 |

90 | { 91 | "I asked her: why did you do it? 92 | Why did you tear apart the only lily pad I'd ever know as home?" 93 | } 94 |

95 | 96 |

97 | { 98 | "She looked back at me with those 99 | froggy little eyes of hers and croaked one word:" 100 | } 101 |

102 | 103 |

104 | {"Pleasure."} 105 |

106 |
107 | ); 108 | };`; 109 | 110 | const UntimedExample = () => { 111 | const { proceed } = useContext(DialogChildContext); 112 | 113 | return ( 114 | 115 |

116 | { 117 | "I asked her: why did you do it? Why did you tear apart the only lily pad I'd ever know as home?" 118 | } 119 |

120 |

121 | { 122 | "She looked back at me with those froggy little eyes of hers and croaked one word:" 123 | } 124 |

125 |

126 | {"Pleasure."} 127 |

128 |
129 | ); 130 | }; 131 | 132 | const PausedExample = () => { 133 | const { proceed } = useContext(DialogChildContext); 134 | 135 | return ( 136 | 137 |

138 | { 139 | "I asked her: why did you do it? Why did you tear apart the only lily pad I'd ever know as home?" 140 | } 141 |

142 | 143 |

144 | { 145 | "She looked back at me with those froggy little eyes of hers and croaked one word:" 146 | } 147 |

148 | 149 |

150 | {"Pleasure."} 151 |

152 |
153 | ); 154 | }; 155 | 156 | const PacingPausing = () => { 157 | return ( 158 |
159 | 160 | 161 | { 162 | "By default, we try to breathe a little life into yer windups by changing the timing depending on which character's being printed." 163 | } 164 | 165 | 166 | {"Without it, it can come off a "} 167 | 168 | {"little bit unnatural"} 169 | {"..."} 170 | 171 | 172 | 173 | 174 | 175 | 176 | {"It just leaves you cold..."} 177 | 178 | {"but then look at it with the default pacing back on:"} 179 | 180 | 181 | 182 | 183 | {"It's a subtle thing that makes a big difference."} 184 | 185 | { 186 | "But maybe you want faster text. Or you have text in a language other than English, which the defaults are built around..." 187 | } 188 | 189 | 190 | {"Luckily we've got the tools to help you out."} 191 | 192 | {"Here's a simple example that slows down between words."} 193 | {PACE_HOOK_EXAMPLE} 194 | 195 | 196 | 197 | 198 | {"pace"} 199 | { 200 | " is just a function that takes a character and returns a number of milliseconds." 201 | } 202 | 203 | 204 | {"Here's something similar with WindupChildren and the "} 205 | {"Pace"} 206 | {" component:"} 207 | 208 | {PACE_COMPONENT_EXAMPLE} 209 | 210 | 211 | 212 | 213 | {"The bonus With WindupChildren is you can "} 214 | 215 | {"pace segments of text differently from each other"} 216 | 217 | {"."} 218 | 219 | 220 | { 221 | "Now it's all very good setting the pace based on what letter's being printed, but sometimes you want to get " 222 | } 223 | {"specific"} 224 | {"."} 225 | 226 | 227 | {"Let's see how we could improve the timing of this windup."} 228 | 229 | 230 | 231 | 232 | 233 | {"It's got potential, but..."} 234 | 235 | 236 | {" the timing just ruins it."} 237 | 238 | 239 | {"Let's tighten it up with the "} 240 | {"Pause"} 241 | {" component."} 242 | 243 | {PAUSED_CODE_EXAMPLE} 244 | 245 | 246 | 247 | 248 | {"Woah."} 249 | 250 | {"Don't it just give you chills?"} 251 | 252 | 253 | { 254 | "And that should give you everything you need to create an award-winning windup. See you in the next guide!" 255 | } 256 | 257 | 258 |
259 | ); 260 | }; 261 | 262 | export default PacingPausing; 263 | -------------------------------------------------------------------------------- /src/guides/WindupsWithAnything.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Section from "../Section"; 3 | import Dialog, { DialogChildContext } from "../Dialog"; 4 | import Frog, { HappyExpression } from "../performers/Frog"; 5 | import CodeExample from "../CodeExample"; 6 | import Example from "../Example"; 7 | import { WindupChildren } from "windups"; 8 | import Auntie from "../images/auntie.svg"; 9 | 10 | const PIC_EXAMPLE = `import React from "react"; 11 | import { WindupChildren } from "windups"; 12 | 13 | const AuntieWindup = () => { 14 | return ( 15 | 16 |
17 | {"My dear auntie"} 18 |
19 | 20 |
21 | {"The world's kindest frog"} 22 |
23 |
24 | ); 25 | };`; 26 | 27 | const COMPOSED_EXAMPLE = `import React from "react"; 28 | import { WindupChildren } from "windups"; 29 | 30 | const ComponentWithInlineText = ({children}) => { 31 | return ( 32 | <> 33 |
{"This text will render immediately!"}
34 |
{children}
35 | 36 | ) 37 | } 38 | 39 | const ComposedWindup = () => { 40 | return ( 41 | 42 | 43 | {"This text will become part of the windup."} 44 | 45 | 46 | ); 47 | };`; 48 | 49 | // Real stuff starts here 50 | 51 | const AuntieWindup = () => { 52 | const { proceed } = useContext(DialogChildContext); 53 | return ( 54 | 55 |
{"My dear auntie"}
56 | 57 |
{"The world's kindest frog"}
58 |
59 | ); 60 | }; 61 | 62 | const ComponentWithInlineText: React.FC = ({ children }) => { 63 | return ( 64 | <> 65 |
{"This text will render immediately!"}
66 |
{children}
67 | 68 | ); 69 | }; 70 | 71 | const ComposedWindup = () => { 72 | const { proceed } = useContext(DialogChildContext); 73 | 74 | return ( 75 | 76 | 77 | {"This text will become part of the windup."} 78 | 79 | 80 | ); 81 | }; 82 | 83 | const WindupsWithAnything: React.FC = () => { 84 | return ( 85 |
86 | 87 | 88 | { 89 | "Because the WindupChildren component has a children prop, you can put just about anything in there." 90 | } 91 | 92 | 93 | { 94 | "But you may be asking 'what's gonna happen if I put a picture of my dear aunt in there?'. Well... let's give it a go." 95 | } 96 | 97 | {PIC_EXAMPLE} 98 | 99 | 100 | 101 | 102 | { 103 | "WindupChildren treats that image as though it's just a character in a sentence." 104 | } 105 | 106 | 107 | {"Now, what if you use a component that has some text in it?"} 108 | 109 | {COMPOSED_EXAMPLE} 110 | 111 | 112 | 113 | 114 | { 115 | "So what's the deal? How come the text passed as children becomes part of the windup, but the text in the component doesn't?" 116 | } 117 | 118 | 119 | { 120 | "That's why WindupChildren is called WindupChildren, see? It can only see what's been passed through a children prop. What a component eventually renders may as well be invisible to it." 121 | } 122 | 123 | 124 | { 125 | "That may seem like a cop-out, but in my experience it's real useful." 126 | } 127 | 128 | { 129 | " Gives you fine-grained control over what goes in the windup and what doesn't." 130 | } 131 | 132 | {"So just remember:"} 133 | 134 | {"Anything passed in a children prop will become part of the windup."} 135 | 136 | 137 | { 138 | "Text passed as children props will be broken up into individual characters." 139 | } 140 | 141 | 142 | {"And text that's rendered by a component through other means won't."} 143 | 144 | 145 | {"With that knowledge, you can get real creative with WindupChildren"} 146 | 147 | 148 |
149 | ); 150 | }; 151 | 152 | export default WindupsWithAnything; 153 | -------------------------------------------------------------------------------- /src/images/auntie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/images/compass-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/images/forks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/frog-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/frog/Group 12-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/frog/Group 12-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/images/frog/Group 12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/frog/Group 13-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/Group 13-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/Group 13.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-cool-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/images/frog/f-cool-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-cool-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/images/frog/f-laff-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-laff-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-laff-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-mad-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/images/frog/f-mad-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/images/frog/f-mad-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-norm-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-norm-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-norm-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-shame-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-shame-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/frog/f-shame-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/frog/f-shock-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/frog/f-shock-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/images/frog/f-shock-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/images/frog/f-smug-open-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-smug-open-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/frog/f-smug-resting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/gwilco.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/key-a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/images/key-b.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/key-c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/key-d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/key-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/keyboard-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/little-hand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/nexters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/images/point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/images/repo-menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/images/ruffle-pizza.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/images/snowman-kebab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /src/performers/Frog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, createContext } from "react"; 2 | import Performer, { PerformerContext } from "../Performer"; 3 | import { DialogElementProps } from "../DialogElement"; 4 | import AvNormResting from "../images/frog/f-norm-resting.svg"; 5 | import AvNorm1 from "../images/frog/f-norm-open-1.svg"; 6 | import AvNorm2 from "../images/frog/f-norm-open-2.svg"; 7 | import AvLaffResting from "../images/frog/f-laff-resting.svg"; 8 | import AvLaff1 from "../images/frog/f-laff-open-1.svg"; 9 | import AvLaff2 from "../images/frog/f-laff-open-2.svg"; 10 | import AvMadResting from "../images/frog/f-mad-resting.svg"; 11 | import AvMad1 from "../images/frog/f-mad-open-1.svg"; 12 | import AvMad2 from "../images/frog/f-mad-open-2.svg"; 13 | import AvSmugResting from "../images/frog/f-smug-resting.svg"; 14 | import AvSmug1 from "../images/frog/f-smug-open-1.svg"; 15 | import AvSmug2 from "../images/frog/f-smug-open-2.svg"; 16 | import AvShameResting from "../images/frog/f-shame-resting.svg"; 17 | import AvShame1 from "../images/frog/f-shame-open-1.svg"; 18 | import AvShame2 from "../images/frog/f-shame-open-2.svg"; 19 | import AvShockResting from "../images/frog/f-shock-resting.svg"; 20 | import AvShock1 from "../images/frog/f-shock-open-1.svg"; 21 | import AvShock2 from "../images/frog/f-shock-open-2.svg"; 22 | import AvCoolResting from "../images/frog/f-cool-resting.svg"; 23 | import AvCool1 from "../images/frog/f-cool-open-1.svg"; 24 | import AvCool2 from "../images/frog/f-cool-open-2.svg"; 25 | import { FrameSet } from "../Performer"; 26 | 27 | import { Effect } from "windups"; 28 | 29 | const normFrames = [AvNorm1, AvNorm2]; 30 | const normRestingFrames = [AvNormResting]; 31 | const laffFrames = [AvLaff1, AvLaff2]; 32 | const laffRestingFrames = [AvLaffResting]; 33 | const madFrames = [AvMad1, AvMad2]; 34 | const madRestingFrames = [AvMadResting]; 35 | const smugFrames = [AvSmug1, AvSmug2]; 36 | const smugRestingFrames = [AvSmugResting]; 37 | const shameFrames = [AvShame1, AvShame2]; 38 | const shameRestingFrames = [AvShameResting]; 39 | const shockFrames = [AvShock1, AvShock2]; 40 | const shockRestingFrames = [AvShockResting]; 41 | const coolFrames = [AvCool1, AvCool2]; 42 | const coolRestingFrames = [AvCoolResting]; 43 | 44 | type SetExpressionProps = { 45 | expression: FrogEmotion; 46 | }; 47 | 48 | const SetExpression: React.FC = ({ expression }) => { 49 | const { setExpression } = useContext(FrogContext); 50 | const { setAvatarFrames } = useContext(PerformerContext); 51 | return ( 52 | { 54 | setExpression(expression); 55 | setAvatarFrames(frogFrameSets[expression]); 56 | }} 57 | /> 58 | ); 59 | }; 60 | 61 | function makeExpression(expr: FrogEmotion) { 62 | return () => ; 63 | } 64 | 65 | export const HappyExpression = makeExpression("HAPPY"); 66 | export const SmugExpression = makeExpression("SMUG"); 67 | export const ShameExpression = makeExpression("SHAME"); 68 | export const ShockExpression = makeExpression("SHOCK"); 69 | export const MadExpression = makeExpression("MAD"); 70 | 71 | type FrogEmotion = 72 | | "NORMAL" 73 | | "HAPPY" 74 | | "MAD" 75 | | "SMUG" 76 | | "SHAME" 77 | | "SHOCK" 78 | | "COOL"; 79 | 80 | type FrogFrameMap = Record; 81 | 82 | const frogFrameSets: FrogFrameMap = { 83 | NORMAL: { normal: normFrames, resting: normRestingFrames }, 84 | HAPPY: { normal: laffFrames, resting: laffRestingFrames }, 85 | MAD: { normal: madFrames, resting: madRestingFrames }, 86 | SMUG: { normal: smugFrames, resting: smugRestingFrames }, 87 | SHAME: { normal: shameFrames, resting: shameRestingFrames }, 88 | SHOCK: { normal: shockFrames, resting: shockRestingFrames }, 89 | COOL: { normal: coolFrames, resting: coolRestingFrames }, 90 | }; 91 | 92 | interface FrogProps extends DialogElementProps { 93 | expression?: FrogEmotion; 94 | silent?: boolean; 95 | } 96 | 97 | const FrogContext = createContext<{ 98 | setExpression: React.Dispatch>; 99 | currentExpression: FrogEmotion; 100 | }>({ 101 | setExpression: () => {}, 102 | currentExpression: "NORMAL", 103 | }); 104 | 105 | const Frog: React.FC = ({ 106 | children, 107 | autoProceed, 108 | expression: initialExpression = "NORMAL", 109 | silent, 110 | }) => { 111 | const [currentExpression, setCurrentExpression] = useState(initialExpression); 112 | 113 | return ( 114 | 117 | 122 | 123 | {children} 124 | 125 | 126 | ); 127 | }; 128 | 129 | export const usePreloadFrogFrames = () => { 130 | const frames = [ 131 | ...normFrames, 132 | ...normRestingFrames, 133 | ...laffFrames, 134 | ...laffRestingFrames, 135 | ...madFrames, 136 | ...madRestingFrames, 137 | ...smugFrames, 138 | ...smugRestingFrames, 139 | ...shameFrames, 140 | ...shameRestingFrames, 141 | ...shockFrames, 142 | ...shockRestingFrames, 143 | ...coolFrames, 144 | ...coolRestingFrames, 145 | ]; 146 | 147 | frames.forEach((source) => { 148 | const image = new Image(); 149 | image.src = source; 150 | }); 151 | }; 152 | 153 | export default Frog; 154 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl) 112 | .then(response => { 113 | // Ensure service worker exists, and that we really are getting a JS file. 114 | const contentType = response.headers.get('content-type'); 115 | if ( 116 | response.status === 404 || 117 | (contentType != null && contentType.indexOf('javascript') === -1) 118 | ) { 119 | // No service worker found. Probably a different app. Reload the page. 120 | navigator.serviceWorker.ready.then(registration => { 121 | registration.unregister().then(() => { 122 | window.location.reload(); 123 | }); 124 | }); 125 | } else { 126 | // Service worker found. Proceed as normal. 127 | registerValidSW(swUrl, config); 128 | } 129 | }) 130 | .catch(() => { 131 | console.log( 132 | 'No internet connection found. App is running in offline mode.' 133 | ); 134 | }); 135 | } 136 | 137 | export function unregister() { 138 | if ('serviceWorker' in navigator) { 139 | navigator.serviceWorker.ready.then(registration => { 140 | registration.unregister(); 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------