├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── design └── slides.sketch ├── package.json ├── public ├── application.js ├── favicon.png ├── images │ ├── about-author.jpg │ ├── actuation.png │ ├── architecture.jpg │ ├── dan-article.jpg │ ├── dialog-close.svg │ ├── dirty-circle.png │ ├── gifs │ │ ├── abstract.gif │ │ ├── bean.gif │ │ ├── factory.gif │ │ ├── robot-dance.gif │ │ ├── storm.gif │ │ └── wave.gif │ ├── guys │ │ ├── alex.svg │ │ ├── avram.svg │ │ ├── gustav.svg │ │ ├── jose.svg │ │ ├── pierrot.svg │ │ └── tikhon.svg │ ├── immutable-chain.png │ ├── immutable-logo.png │ ├── immutable-map.png │ ├── moon-emoji.png │ ├── producthunt-badge.jpg │ ├── rams-radio.jpg │ ├── resume-screen-1.jpg │ ├── resumeio-logo.png │ ├── sequence.jpg │ ├── state-map.png │ ├── stateful-circle.png │ ├── stateless-circle.png │ ├── stateless-square.png │ ├── sun-emoji.png │ ├── sunglasses-emoji.png │ ├── talks │ │ ├── acko.gif │ │ ├── aerotwist.gif │ │ ├── anim-vdom.gif │ │ ├── eyeo.gif │ │ ├── fsm.gif │ │ └── immut-uis.gif │ ├── timeline-ideal.jpg │ ├── timeline-raf.jpg │ └── timeline-set-timeout.jpg ├── index.html └── sounds │ ├── game-start.ogg │ ├── slap.ogg │ └── whoop.mp3 ├── src ├── application.js ├── blocks │ ├── button.js │ ├── dynamic-code.js │ ├── figure-caption.js │ ├── frame-background.js │ └── layout.js ├── colors.js ├── ficus │ ├── bubble-poll │ │ ├── index.js │ │ └── point-force.js │ ├── classic-poll │ │ ├── classic-poll.js │ │ ├── classic-poll.scss │ │ └── index.js │ ├── cloud-poll │ │ ├── cloud-poll.scss │ │ └── index.js │ ├── ficus-poll-adapter.js │ └── simple-poll │ │ ├── index.js │ │ ├── simple-poll.js │ │ └── simple-poll.scss ├── presentation.js └── slides │ ├── actuation │ ├── action-logger.js │ ├── talking-heads.js │ └── this-guy.js │ ├── boids │ ├── index.js │ ├── neighbour-detector.js │ └── simulator.js │ ├── enter-exit │ ├── 01-dialog-enter.js │ ├── 02-dialog-exit.js │ ├── animated.js │ ├── fake-dialog.js │ ├── index.js │ └── state-monitor.js │ ├── etc │ ├── animation-expectations.js │ ├── golden-rule.js │ ├── links.js │ ├── resources.js │ └── summary.js │ ├── flip │ ├── animated-route.js │ └── index.js │ ├── golden-rule │ └── index.js │ ├── motion-ghost-slide.js │ ├── own-render │ ├── index.js │ └── rotator.js │ ├── poll-slides.js │ ├── poll-slides.scss │ ├── raf-vs-timeout │ ├── comparison-slide.js │ ├── index.js │ ├── raf-slides.js │ ├── rolling-meter.js │ ├── timeline-with-meter.js │ └── timeline.js │ ├── transistor.js │ ├── transistor.scss │ └── transitions │ └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest/globals": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 2016, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": true 14 | } 15 | }, 16 | "plugins": ["react", "prettier", "jest"], 17 | "extends": [ 18 | "standard", 19 | "plugin:react/recommended", 20 | "prettier", 21 | "prettier/flowtype", 22 | "prettier/react" 23 | ], 24 | "rules": { 25 | "react/prop-types": "off", 26 | "prettier/prettier": [ 27 | "error", 28 | { 29 | "singleQuote": true, 30 | "semi": false 31 | } 32 | ], 33 | "max-len": [ 34 | "warn", 35 | { 36 | "code": 80, 37 | "ignoreUrls": true 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,osx 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | ### OSX ### 54 | *.DS_Store 55 | .AppleDouble 56 | .LSOverride 57 | 58 | # Icon must end with two \r 59 | Icon 60 | # Thumbnails 61 | ._* 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | # Directories potentially created on remote AFP share 71 | .AppleDB 72 | .AppleDesktop 73 | Network Trash Folder 74 | Temporary Items 75 | .apdisk 76 | 77 | 78 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Animations in a Stateful World 2 | 3 | Slides and demos. 4 | -------------------------------------------------------------------------------- /design/slides.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/design/slides.sketch -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateful-animations", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "webpack-dev-server", 7 | "build": "NODE_ENV=production webpack --progress", 8 | "deploy": "gh-pages-deploy" 9 | }, 10 | "gh-pages-deploy": { 11 | "staticpath": "public", 12 | "prep": ["build"], 13 | "noprompt": false 14 | }, 15 | "dependencies": { 16 | "classnames": "^2.2.5", 17 | "d3-array": "^1.0.1", 18 | "d3-ease": "^1.0.2", 19 | "d3-force": "^1.0.3", 20 | "d3-interpolate": "^1.1.5", 21 | "d3-selection": "^1.0.2", 22 | "d3-transition": "^1.0.3", 23 | "howler": "^2.0.2", 24 | "lodash": "^4.17.2", 25 | "polished": "^1.9.0", 26 | "presa": "1.0.3", 27 | "react": "^16.2.0", 28 | "react-dom": "^16.2.0", 29 | "react-motion": "^0.5.2", 30 | "react-redux": "^5.0.6", 31 | "react-syntax-highlighter": "^6.0.4", 32 | "react-transition-group": "^2.2.1", 33 | "redux": "^3.6.0", 34 | "redux-actuator": "^2.0.1", 35 | "styled-components": "^2.2.4", 36 | "three": "^0.88.0", 37 | "velocity-animate": "^1.3.1" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.14.0", 41 | "babel-core": "^6.14.0", 42 | "babel-eslint": "^8.0.2", 43 | "babel-loader": "^7.1.2", 44 | "babel-polyfill": "^6.16.0", 45 | "babel-preset-es2015": "^6.14.0", 46 | "babel-preset-react": "^6.11.1", 47 | "babel-preset-stage-0": "^6.16.0", 48 | "css-loader": "^0.28.7", 49 | "eslint": "^4.11.0", 50 | "eslint-config-prettier": "^2.7.0", 51 | "eslint-config-standard": "^10.2.1", 52 | "eslint-plugin-import": "^2.8.0", 53 | "eslint-plugin-jest": "^21.3.2", 54 | "eslint-plugin-node": "^5.2.1", 55 | "eslint-plugin-prettier": "^2.3.1", 56 | "eslint-plugin-promise": "^3.6.0", 57 | "eslint-plugin-react": "^7.4.0", 58 | "eslint-plugin-standard": "^3.0.1", 59 | "gh-pages-deploy": "^0.4.2", 60 | "node-sass": "^4.7.2", 61 | "prettier": "^1.8.2", 62 | "sass-loader": "^6.0.6", 63 | "style-loader": "^0.19.0", 64 | "uglifyjs-webpack-plugin": "^1.1.2", 65 | "webpack": "^3.8.1", 66 | "webpack-dev-server": "^2.9.5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/favicon.png -------------------------------------------------------------------------------- /public/images/about-author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/about-author.jpg -------------------------------------------------------------------------------- /public/images/actuation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/actuation.png -------------------------------------------------------------------------------- /public/images/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/architecture.jpg -------------------------------------------------------------------------------- /public/images/dan-article.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/dan-article.jpg -------------------------------------------------------------------------------- /public/images/dialog-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/images/dirty-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/dirty-circle.png -------------------------------------------------------------------------------- /public/images/gifs/abstract.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/abstract.gif -------------------------------------------------------------------------------- /public/images/gifs/bean.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/bean.gif -------------------------------------------------------------------------------- /public/images/gifs/factory.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/factory.gif -------------------------------------------------------------------------------- /public/images/gifs/robot-dance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/robot-dance.gif -------------------------------------------------------------------------------- /public/images/gifs/storm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/storm.gif -------------------------------------------------------------------------------- /public/images/gifs/wave.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/gifs/wave.gif -------------------------------------------------------------------------------- /public/images/guys/alex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | alex 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/images/guys/avram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Avram 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/images/guys/gustav.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gustav 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /public/images/guys/jose.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hose 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/images/guys/pierrot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pier 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/images/guys/tikhon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | me 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/images/immutable-chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/immutable-chain.png -------------------------------------------------------------------------------- /public/images/immutable-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/immutable-logo.png -------------------------------------------------------------------------------- /public/images/immutable-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/immutable-map.png -------------------------------------------------------------------------------- /public/images/moon-emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/moon-emoji.png -------------------------------------------------------------------------------- /public/images/producthunt-badge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/producthunt-badge.jpg -------------------------------------------------------------------------------- /public/images/rams-radio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/rams-radio.jpg -------------------------------------------------------------------------------- /public/images/resume-screen-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/resume-screen-1.jpg -------------------------------------------------------------------------------- /public/images/resumeio-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/resumeio-logo.png -------------------------------------------------------------------------------- /public/images/sequence.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/sequence.jpg -------------------------------------------------------------------------------- /public/images/state-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/state-map.png -------------------------------------------------------------------------------- /public/images/stateful-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/stateful-circle.png -------------------------------------------------------------------------------- /public/images/stateless-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/stateless-circle.png -------------------------------------------------------------------------------- /public/images/stateless-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/stateless-square.png -------------------------------------------------------------------------------- /public/images/sun-emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/sun-emoji.png -------------------------------------------------------------------------------- /public/images/sunglasses-emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/sunglasses-emoji.png -------------------------------------------------------------------------------- /public/images/talks/acko.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/acko.gif -------------------------------------------------------------------------------- /public/images/talks/aerotwist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/aerotwist.gif -------------------------------------------------------------------------------- /public/images/talks/anim-vdom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/anim-vdom.gif -------------------------------------------------------------------------------- /public/images/talks/eyeo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/eyeo.gif -------------------------------------------------------------------------------- /public/images/talks/fsm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/fsm.gif -------------------------------------------------------------------------------- /public/images/talks/immut-uis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/talks/immut-uis.gif -------------------------------------------------------------------------------- /public/images/timeline-ideal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/timeline-ideal.jpg -------------------------------------------------------------------------------- /public/images/timeline-raf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/timeline-raf.jpg -------------------------------------------------------------------------------- /public/images/timeline-set-timeout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/images/timeline-set-timeout.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Animations in a stateful world 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /public/sounds/game-start.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/sounds/game-start.ogg -------------------------------------------------------------------------------- /public/sounds/slap.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/sounds/slap.ogg -------------------------------------------------------------------------------- /public/sounds/whoop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/public/sounds/whoop.mp3 -------------------------------------------------------------------------------- /src/application.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import Presentation from './presentation' 5 | import { injectGlobal } from 'styled-components' 6 | 7 | injectGlobal` 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | ` 13 | 14 | document.addEventListener('DOMContentLoaded', function() { 15 | ReactDOM.render(, document.querySelector('.application')) 16 | }) 17 | -------------------------------------------------------------------------------- /src/blocks/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styled from 'styled-components' 4 | import { rgba } from 'polished' 5 | 6 | const Button = ({ icon, children, ...props }) => ( 7 | 8 | {icon && {icon}} 9 | {children} 10 | 11 | ) 12 | 13 | const borderColor = '#edbfc0' 14 | 15 | const ButtonIcon = styled.span` 16 | font-size: 24px; 17 | line-height: 21px; 18 | vertical-align: middle; 19 | margin-right: 8px; 20 | ` 21 | 22 | const ButtonContainer = styled.button` 23 | border-radius: 6px; 24 | border: 3px solid ${borderColor}; 25 | 26 | display: inline-flex; 27 | align-items: center; 28 | 29 | cursor: pointer; 30 | user-select: none; 31 | outline: none; 32 | 33 | background-color: white; 34 | color: black; 35 | font-family: inherit; 36 | 37 | padding: 9px 11px; 38 | font-size: 18px; 39 | margin: 4px; 40 | 41 | &:hover { 42 | background-color: ${rgba(borderColor, 0.01)}; 43 | } 44 | 45 | &:active { 46 | background-color: ${rgba(borderColor, 0.1)}; 47 | } 48 | 49 | ${props => 50 | props.checked && 51 | ` 52 | &, 53 | &:active, 54 | &:hover { 55 | background-color: ${rgba('#6edda4', 0.2)}; 56 | } 57 | `}; 58 | ` 59 | 60 | export default Button 61 | -------------------------------------------------------------------------------- /src/blocks/dynamic-code.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { rgba } from 'polished' 4 | 5 | // TODO: 6 | // Refactor this one, rewrite entirely with styled comp. 7 | // Think about the way to use tag literal. 8 | // Something like: 9 | //
10 | // DynamicCode` 11 | // 12 | // ${this.props.text + 'px'} 13 | // 14 | // ` 15 | //
16 | 17 | const Code = styled.span` 18 | font-family: ${props => props.theme.monoFont}; 19 | white-space: pre; 20 | font-size: 20px; 21 | 22 | .code-preview__highlight { 23 | background-color: rgba(0, 0, 0, 0); 24 | transition: background-color 1.6s ease; 25 | border-radius: 2px; 26 | } 27 | 28 | .code-preview__highlight--flash { 29 | transition-duration: 0.15s; 30 | background-color: ${rgba('#fc7979', 0.5)}; 31 | } 32 | ` 33 | 34 | const DynamicCode = ({ children }) => {children} 35 | 36 | export class HightlightSegment extends Component { 37 | componentDidUpdate(prevProps) { 38 | if (!this.$root) { 39 | return 40 | } 41 | 42 | if (!this.props.pure || this.props.text !== prevProps.text) { 43 | this.$root.classList.add('code-preview__highlight--flash') 44 | 45 | // No good :) 46 | setTimeout( 47 | () => 48 | this.$root && 49 | this.$root.classList.remove('code-preview__highlight--flash'), 50 | 100 51 | ) 52 | } 53 | } 54 | 55 | render() { 56 | return ( 57 | { 59 | this.$root = e 60 | }} 61 | className="code-preview__highlight" 62 | > 63 | {this.props.text} 64 | 65 | ) 66 | } 67 | } 68 | 69 | DynamicCode.H = HightlightSegment 70 | export default DynamicCode 71 | -------------------------------------------------------------------------------- /src/blocks/figure-caption.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const FigureCaption = styled.div` 4 | padding: 24px; 5 | font-size: 22px; 6 | min-width: 240px; 7 | max-width: 80%; 8 | text-align: center; 9 | margin: 0 auto; 10 | line-height: 1.4; 11 | ` 12 | 13 | export default FigureCaption 14 | -------------------------------------------------------------------------------- /src/blocks/frame-background.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const FrameBackground = styled.iframe` 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | 10 | overflow: hidden; 11 | border: none; 12 | ` 13 | 14 | export default FrameBackground 15 | -------------------------------------------------------------------------------- /src/blocks/layout.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Layout = styled.div` 4 | width: 100%; 5 | height: 100%; 6 | 7 | padding: 60px 80px; 8 | box-sizing: border-box; 9 | ` 10 | 11 | export default Layout 12 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | green: '#00C967', 3 | gray: '#CFCFCF', 4 | textGray: '#656565', 5 | red: '#FD6669', 6 | yellow: '#FFCC33', 7 | black: '#000000', 8 | pink: '#FB799D' 9 | } 10 | 11 | export default colors 12 | -------------------------------------------------------------------------------- /src/ficus/bubble-poll/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { 4 | forceSimulation, 5 | forceCollide, 6 | forceCenter 7 | } from 'd3-force' 8 | 9 | import pointForce from './point-force' 10 | 11 | class BubblePoll extends Component { 12 | componentDidUpdate () { 13 | this.nodes = this.props.poll.voters.map((v, idx) => { 14 | let node = this.nodes[idx] ? this.nodes[idx] : {} 15 | 16 | return { 17 | r: Math.random() * 4 + 10, 18 | x: 0, 19 | y: 0, 20 | ...node, 21 | choice: v.choice 22 | } 23 | }) 24 | 25 | this.choicesIds = this.props.poll.choices.map(ch => ch.id) 26 | 27 | this.simulation.stop() 28 | this.simulation.nodes(this.nodes) 29 | this.simulation.restart() 30 | } 31 | 32 | componentWillUnmount () { 33 | this.simulation.stop() 34 | } 35 | 36 | onTick () { 37 | const canvas = this.pollEl.querySelector('canvas') 38 | const context = canvas.getContext('2d') 39 | 40 | const width = canvas.width 41 | const height = canvas.height 42 | 43 | context.clearRect(0, 0, width, height) 44 | context.save() 45 | context.translate(width / 2, height / 2) 46 | 47 | this.props.poll.choices.forEach(choice => { 48 | context.beginPath() 49 | 50 | this.nodes.forEach(function (d) { 51 | if (d.choice === choice.id) { 52 | context.moveTo(d.x + d.r, d.y) 53 | context.arc(d.x, d.y, d.r, 0, 2 * Math.PI) 54 | } 55 | }) 56 | 57 | context.fillStyle = choice.color 58 | context.fill() 59 | }) 60 | 61 | context.restore() 62 | } 63 | 64 | nodeTarget (node) { 65 | const r = 220 66 | 67 | const idx = this.choicesIds.indexOf(node.choice) 68 | 69 | return [ 70 | r * Math.cos(idx * 2 * Math.PI / this.choicesIds.length), 71 | r * Math.sin(idx * 2 * Math.PI / this.choicesIds.length), 72 | 0.015 73 | ] 74 | } 75 | 76 | componentDidMount () { 77 | this.nodes = this.props.poll.voters.map((n) => ({ 78 | r: Math.random() * 4 + 6, 79 | choice: n.choice 80 | })) 81 | 82 | this.choicesIds = this.props.poll.choices.map(ch => ch.id) 83 | 84 | this.simulation = forceSimulation(this.nodes) 85 | .velocityDecay(0.2) 86 | .alphaDecay(0.00001) 87 | .force('target', pointForce(x => this.nodeTarget(x))) 88 | .force('collide', forceCollide().radius(function (d) { return d.r + 3 }).iterations(2)) 89 | .force('center', forceCenter()) 90 | .on('tick', () => this.onTick()) 91 | } 92 | 93 | render () { 94 | return ( 95 |
{ this.pollEl = e }} 97 | className='bubble-poll'> 98 | 99 |
) 100 | } 101 | } 102 | 103 | export default BubblePoll 104 | 105 | -------------------------------------------------------------------------------- /src/ficus/bubble-poll/point-force.js: -------------------------------------------------------------------------------- 1 | export default function (fn) { 2 | var nodes 3 | 4 | function force (alpha) { 5 | for (var i = 0, n = nodes.length, node; i < n; ++i) { 6 | node = nodes[i] 7 | let [tx, ty, strength] = fn(node) 8 | 9 | strength = strength || 0.1 10 | 11 | node.vx += (tx - node.x) * strength * alpha 12 | node.vy += (ty - node.y) * strength * alpha 13 | } 14 | } 15 | 16 | force.initialize = function (_) { 17 | nodes = _ 18 | if (!nodes) return 19 | } 20 | 21 | return force 22 | } 23 | -------------------------------------------------------------------------------- /src/ficus/classic-poll/classic-poll.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | class ClassicPoll extends Component { 4 | render () { 5 | const { poll } = this.props 6 | 7 | const results = poll.choices.map(choice => ({ 8 | ...choice, 9 | ...poll.results[choice.id] 10 | })) 11 | 12 | return ( 13 |
14 |
15 | {poll.title} 16 |
17 | 18 |
19 | Для отправки голоса пройдите по ссылке 20 | {poll.url} 21 |
22 | 23 |
24 | {results.map(result => ( 25 |
26 |
27 |
28 | {result.text} 29 |
30 | 31 |
32 |
36 |
37 |
38 | 39 |
40 | {`${(result.percent * 100).toFixed(0)}%`} 41 |
42 |
43 | 44 | ))} 45 |
46 |
47 | ) 48 | } 49 | } 50 | 51 | export default ClassicPoll 52 | -------------------------------------------------------------------------------- /src/ficus/classic-poll/classic-poll.scss: -------------------------------------------------------------------------------- 1 | .classic-poll { 2 | width: 1024px; 3 | height: 768px; 4 | display: flex; 5 | flex-flow: column nowrap; 6 | align-items: stretch; 7 | font-family: Helvetica; 8 | padding: 30px 40px; 9 | box-sizing: border-box; 10 | } 11 | 12 | .classic-poll__question { 13 | font-size: 42px; 14 | font-weight: bold; 15 | padding-bottom: 8px; 16 | } 17 | 18 | .classic-poll__subheader { 19 | font-size: 26px; 20 | margin-bottom: 36px; 21 | } 22 | 23 | .classic-poll__subheader-link { 24 | font-weight: bold; 25 | text-decoration: underline; 26 | margin-left: 8px; 27 | padding: 2px 5px; 28 | font-size: 28px; 29 | background-color: #fdfd3d; 30 | } 31 | 32 | .classic-poll__result { 33 | margin-bottom: 24px; 34 | display: flex; 35 | flex-direction: row; 36 | flex-wrap: nowrap; 37 | } 38 | 39 | .classic-poll__line { 40 | flex-grow: 1; 41 | flex-shrink: 1; 42 | } 43 | 44 | .classic-poll__percent { 45 | font-size: 42px; 46 | font-weight: bold; 47 | padding-left: 25px; 48 | width: 120px; 49 | align-self: center; 50 | flex-shrink: 0; 51 | text-align: left; 52 | } 53 | 54 | .classic-poll__result-header { 55 | font-size: 34px; 56 | font-weight: bold; 57 | margin-bottom: 4px; 58 | } 59 | 60 | .classic-poll__progress-wrap { 61 | overflow: hidden; 62 | height: 15px; 63 | border-radius: 5px; 64 | background-color: #eee; 65 | } 66 | 67 | .classic-poll__progress { 68 | height: 15px; 69 | background-color: #000; 70 | transition: transform 0.3s ease; 71 | transform-origin: left top; 72 | } 73 | -------------------------------------------------------------------------------- /src/ficus/classic-poll/index.js: -------------------------------------------------------------------------------- 1 | import ClassicPoll from './classic-poll' 2 | import './classic-poll.scss' 3 | 4 | export default ClassicPoll 5 | -------------------------------------------------------------------------------- /src/ficus/cloud-poll/cloud-poll.scss: -------------------------------------------------------------------------------- 1 | 2 | .cloud-poll { 3 | box-sizing: border-box; 4 | background-color: black; 5 | color: white; 6 | text-align: left; 7 | position: relative; 8 | 9 | display: flex; 10 | flex-flow: column nowrap; 11 | } 12 | 13 | .cloud-poll__poll-question { 14 | padding: 40px; 15 | padding-bottom: 0; 16 | font-size: 40px; 17 | display: flex; 18 | justify-content: center; 19 | flex-shrink: 0; 20 | } 21 | 22 | .cloud-poll__cloud-poll-layout { 23 | flex-grow: 1; 24 | flex-shrink: 1; 25 | display: flex; 26 | flex-flow: row nowrap; 27 | align-items: center; 28 | } 29 | 30 | .cloud-poll__contenders { 31 | height: 100%; 32 | flex: 2 0; 33 | } 34 | 35 | .cloud-poll__summary { 36 | flex: 1 0; 37 | height: 100%; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .cloud-poll__balls-container { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | z-index: 2; 48 | } 49 | 50 | .cloud-poll__ball { 51 | position: absolute; 52 | top: -9px; 53 | left: -9px; 54 | width: 18px; 55 | height: 18px; 56 | border-radius: 18px; 57 | opacity: 0; 58 | background-color: white; 59 | } 60 | 61 | .cloud-poll__contenders { 62 | position: relative; 63 | } 64 | 65 | // --- 66 | // Cloud poll entry 67 | // --- 68 | $lineFontSize: 20px; 69 | 70 | .cloud-poll__entry { 71 | display: flex; 72 | flex-flow: row nowrap; 73 | align-items: center; 74 | 75 | position: absolute; 76 | top: 0; 77 | left: 0; 78 | line-height: 1.222; 79 | } 80 | 81 | .cloud-poll__entry-name { 82 | padding-left: 20px;; 83 | box-sizing: border-box; 84 | width: 350px; 85 | font-size: $lineFontSize; 86 | margin-right: 9px; 87 | text-align: right; 88 | flex-shrink: 0; 89 | line-height: 20px; 90 | } 91 | 92 | .cloud-poll__entry-votes { 93 | background-color: white; 94 | height: 6px; 95 | border-radius: 6px; 96 | display: inline-block; 97 | width: 23px; 98 | } 99 | 100 | .cloud-poll__entry-count { 101 | z-index: 3; 102 | display: inline-block; 103 | margin-left: -9px; 104 | font-size: $lineFontSize; 105 | border-radius: 20px; 106 | background-color: white; 107 | color: black; 108 | padding-right: 10px; 109 | padding-left: 10px; 110 | min-width: 10px; 111 | height: 25px; 112 | line-height: 25px; 113 | text-align: center; 114 | font-weight: 500; 115 | display: inline-block; 116 | } 117 | 118 | .cloud-poll__already-voted { 119 | text-align: center; 120 | max-width: 140px; 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | justify-content: center; 125 | } 126 | 127 | // --- 128 | // Number of voters and animation 129 | // --- 130 | @keyframes cloud-poll__flash-anim { 131 | 30% { 132 | transform: scale(1.5, 1.5); 133 | } 134 | 135 | 100% { 136 | transform: scale(1.0, 1.0); 137 | } 138 | } 139 | 140 | .cloud-poll__flash-animation { 141 | animation: cloud-poll__flash-anim 0.3s; 142 | animation-timing-function: ease; 143 | } 144 | 145 | 146 | .cloud-poll__number { 147 | font-size: 80px; 148 | font-weight: 900; 149 | } 150 | 151 | .cloud-poll__label { 152 | font-size: 24px; 153 | line-height: 1.222; 154 | } 155 | 156 | .cloud-poll__label-link { 157 | font-size: 24px; 158 | line-height: 1.222; 159 | text-decoration: underline; 160 | font-weight: bold; 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/ficus/cloud-poll/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import _ from 'lodash' 3 | 4 | import { select as d3select } from 'd3-selection' 5 | import { sum as d3sum } from 'd3-array' 6 | import { easeCubic, easeBack, easeElastic } from 'd3-ease' 7 | import 'd3-transition' 8 | 9 | import './cloud-poll.scss' 10 | 11 | function safeFraction (a, b) { 12 | if (b === 0) return 0 13 | return (a / b) 14 | } 15 | 16 | const NAME_WIDTH = 350 17 | const BARS_MARGIN = 15 18 | const BAR_MIN_VALUE = 0.05 19 | 20 | const BALLS_ANIM_DURATION = 1000 21 | const REORDER_ANIM_DURATION = 500 22 | const BAR_GROW_ANIM_DURATION = 500 23 | 24 | class CloudPoll extends React.Component { 25 | render () { 26 | const { poll } = this.props 27 | const votesText = 'votes on' 28 | 29 | const cloudPollStyle = { 30 | width: `${this.props.width}px`, 31 | height: `${this.props.height}px` 32 | } 33 | 34 | return ( 35 |
{ this.$el = n }}> 36 |
{poll.title}
37 |
{ this.$ballsContainer = n }} /> 38 |
39 |
{ this.$contenders = n }} /> 40 |
41 |
42 |
{ this.$alreadyVotedNumber = n }}> 43 | {poll.votersCount} 44 |
45 |
{votesText}
46 |
{poll.url}
47 |
48 |
49 |
50 |
51 | ) 52 | } 53 | 54 | // --- 55 | // Считает, кто на сколько поменялся 56 | // [ 57 | // { name: 'Obama', id: 1, count: 3 }, 58 | // { name: 'Hillary', id: 2, count: -2 } 59 | // ] 60 | // --- 61 | votersDiff (prevProps) { 62 | if (!prevProps || !prevProps.poll) { 63 | return [] 64 | } 65 | 66 | const poll = this.props.poll 67 | const pollWas = prevProps.poll 68 | 69 | let diff = [] 70 | 71 | for (const choiceId in poll.results) { 72 | const d = (poll.results[choiceId].votes - pollWas.results[choiceId].votes) || 0 73 | 74 | if (d !== 0) { 75 | diff.push({ 76 | id: choiceId, 77 | count: d 78 | }) 79 | } 80 | } 81 | 82 | return diff 83 | } 84 | 85 | // --- 86 | // Высчитывает верхнюю границу для значений голосовалки 87 | // --- 88 | calcBoundary (max) { 89 | return 10 * Math.ceil(max / 10) 90 | } 91 | 92 | componentDidMount () { 93 | this.updateVotes() 94 | } 95 | 96 | componentDidUpdate (prevProps) { 97 | this.updateVotes(prevProps) 98 | } 99 | 100 | // Позволяет вычислить offset относительно контейнера опроса 101 | // Работает, если график был трансформирован через transform 102 | relativeOffset (el) { 103 | let x = 0 104 | let y = 0 105 | 106 | const detectClass = this.props.offsetContainerClass 107 | 108 | while (el && el.className.indexOf(detectClass) === -1) { 109 | x += el.offsetLeft 110 | y += el.offsetTop 111 | el = el.offsetParent 112 | } 113 | 114 | return [ x, y ] 115 | } 116 | 117 | updateVotes (prevProps) { 118 | const { poll, alphaTime } = this.props 119 | const prevPoll = prevProps ? prevProps.poll : null 120 | 121 | const newVotes = this.votersDiff(prevProps) 122 | 123 | const contendersHeight = this.$contenders.offsetHeight 124 | const contendersWidth = this.$contenders.offsetWidth 125 | 126 | const barMaxWidth = contendersWidth - NAME_WIDTH - 20 127 | 128 | const entries = _.sortBy( 129 | poll.choices.map(c => ({ ...c, count: poll.results[c.id].votes })), 130 | d => -d.votes) 131 | 132 | let root = d3select(this.$contenders) 133 | 134 | let ballsEnter = d3select(this.$ballsContainer) 135 | .selectAll('.cloud-poll__ball') 136 | .data(newVotes, d => `${d.id} ${d.count}`) 137 | .enter() 138 | .append('div') 139 | .attr('class', 'cloud-poll__ball') 140 | 141 | ballsEnter 142 | .transition() 143 | .duration(alphaTime * BALLS_ANIM_DURATION) 144 | .delay((d, i) => alphaTime * 100 * i) 145 | .attrTween('style', (d, i) => { 146 | return (t) => { 147 | let aEl = this.$alreadyVotedNumber 148 | let bEl = this.$el.querySelector(`[data-id="${d.id}"] .cloud-poll__entry-count`) 149 | 150 | if (!aEl || !bEl) { 151 | return 152 | } 153 | 154 | const aOffset = this.relativeOffset(aEl) 155 | const bOffset = this.relativeOffset(bEl) 156 | 157 | const aX = aOffset[0] + 0.5 * aEl.offsetWidth 158 | const aY = aOffset[1] + 0.5 * aEl.offsetHeight 159 | 160 | const bX = bOffset[0] + 0.5 * bEl.offsetWidth 161 | const bY = bOffset[1] + 0.5 * bEl.offsetHeight 162 | 163 | let tx = easeCubic(t) 164 | let ty = t 165 | 166 | if (d.count < 0) { 167 | tx = 1 - tx 168 | ty = 1 - ty 169 | } 170 | 171 | const x = aX + (bX - aX) * tx 172 | const y = aY + (bY - aY) * ty 173 | 174 | const CP_1 = 0.2 175 | const CP_2 = 0.01 176 | 177 | let scale = 1.0 178 | let opacity = 1.0 179 | 180 | if (t >= 0 && t <= CP_1) { 181 | opacity = (t / CP_1) 182 | scale = opacity 183 | } 184 | 185 | if (t >= (1.0 - CP_2) && t <= 1.0) { 186 | opacity = 1 - ((t - (1.0 - CP_2)) / CP_2) 187 | scale = opacity 188 | } 189 | 190 | let transform = `translate3d(${x}px, ${y}px, 0px) scale(${scale}, ${scale})` 191 | 192 | return ` 193 | opacity: ${opacity}; 194 | transform: ${transform}; 195 | -webkit-transform: ${transform}; 196 | ` 197 | } 198 | }) 199 | .remove() 200 | 201 | // Анимируем взрывающиеся шарики с количество проголосовавших 202 | const scaleTween = (a, b) => { 203 | return (t) => { 204 | let x = a + (b - a) * t 205 | return `transform: scale(${x}, ${x})` 206 | } 207 | } 208 | 209 | ballsEnter.each((data, i) => { 210 | let optionId = data.id 211 | 212 | let entrySelect = root.select(`.cloud-poll__entry[data-id="${optionId}"]`) 213 | 214 | const scaleRatio = 1.3 215 | 216 | entrySelect.select('.cloud-poll__entry-count') 217 | .transition() 218 | .delay(data.count > 0 219 | ? alphaTime * 600 + alphaTime * 100 * i : 0) 220 | .duration(alphaTime * 300) 221 | .ease(easeBack) 222 | .attrTween('style', (d, i) => { return scaleTween(1.0, scaleRatio) }) 223 | .on('end', function () { 224 | d3select(this) 225 | .transition() 226 | .duration(alphaTime * 600) 227 | .ease(easeElastic) 228 | .attrTween('style', () => scaleTween(scaleRatio, 1.0)) 229 | }) 230 | }) 231 | 232 | const allVotes = entries.map(e => e.count) 233 | let max = this.calcBoundary(Math.max(...allVotes)) 234 | 235 | // Flash counter animation 236 | if (prevPoll && prevPoll.votersCount && prevPoll.votersCount !== poll.votersCount) { 237 | this.$alreadyVotedNumber.classList.remove('cloud-poll__flash-animation') 238 | setTimeout(() => { this.$alreadyVotedNumber.classList.add('cloud-poll__flash-animation') }, 0) 239 | } 240 | 241 | let entry = root 242 | .selectAll('.cloud-poll__entry') 243 | .data(entries, e => e.id) 244 | 245 | let barWidthFunc = (d, i) => { 246 | let x = safeFraction(d.count, max) 247 | 248 | let bmin = BAR_MIN_VALUE * barMaxWidth 249 | let w = bmin + (barMaxWidth - bmin) * x 250 | let r = `${w.toFixed(0)}px` 251 | 252 | return r 253 | } 254 | 255 | let entryEnter = entry.enter() 256 | .append('div') 257 | .attr('class', 'cloud-poll__entry') 258 | .attr('data-id', (d) => d.id) 259 | .html(d => ( 260 | ` 261 |
${d.text}
262 |
263 |
${d.count}
264 | ` 265 | )) 266 | 267 | entry.exit().remove() 268 | 269 | let elemHeights = [] 270 | root.selectAll('.cloud-poll__entry').each(function () { 271 | elemHeights.push(this.offsetHeight) 272 | }) 273 | 274 | let positionFunc = (d, i) => { 275 | let fullHeight = d3sum(elemHeights, (h) => h + BARS_MARGIN) 276 | let elemOffset = d3sum(elemHeights.slice(0, i), (h) => h + BARS_MARGIN) 277 | 278 | let y = 0.5 * contendersHeight - 0.5 * fullHeight + elemOffset 279 | return `${y}px` 280 | } 281 | 282 | entryEnter 283 | .style('top', positionFunc) 284 | 285 | entry.select('.cloud-poll__entry-votes') 286 | .transition() 287 | .delay(alphaTime * BALLS_ANIM_DURATION) 288 | .duration(alphaTime * BAR_GROW_ANIM_DURATION) 289 | .style('width', barWidthFunc) 290 | 291 | entry 292 | .transition() 293 | .duration(alphaTime * REORDER_ANIM_DURATION) 294 | .delay(alphaTime * (BALLS_ANIM_DURATION + BAR_GROW_ANIM_DURATION)) 295 | .style('top', positionFunc) 296 | 297 | entry.select('.cloud-poll__entry-count') 298 | .transition('text-trans') 299 | .delay(alphaTime * BALLS_ANIM_DURATION) 300 | .text((d) => d.count) 301 | } 302 | } 303 | 304 | CloudPoll.defaultProps = { 305 | alphaTime: 1.4, 306 | width: 1024, 307 | height: 768, 308 | offsetContainerClass: 'playground__poll' 309 | } 310 | 311 | export default CloudPoll 312 | -------------------------------------------------------------------------------- /src/ficus/ficus-poll-adapter.js: -------------------------------------------------------------------------------- 1 | // This adapter helps to use next-gen poll components 2 | // inside Ficus presenter. It basically transforms 3 | // props to the appropriate format. 4 | const FicusPollAdapter = (Component) => 5 | (props) => { 6 | const results = props.results || {} 7 | const config = props.config || {} 8 | const votesByChoice = results.results || {} 9 | const votersCount = results.votersCount || 0 10 | const extResults = {} 11 | 12 | const configPoll = config.poll || [] 13 | 14 | configPoll.forEach(p => { 15 | extResults[p.id] = { 16 | votes: votesByChoice[p.id], 17 | percent: (votersCount ? votesByChoice[p.id] / votersCount : 0.0) 18 | } 19 | }) 20 | 21 | // Ficus presenter doesn't let use see the vote 22 | // of a specific user, but instead gives you a whole 23 | // poll summary 24 | const voters = [] 25 | 26 | const poll = { 27 | title: config.title, 28 | url: props.url, 29 | 30 | choices: configPoll.map(p => ({ 31 | id: p.id, 32 | color: p.color, 33 | text: p.text 34 | })), 35 | 36 | voters: voters, 37 | 38 | votersCount: votersCount, 39 | results: extResults 40 | } 41 | 42 | return 43 | } 44 | 45 | export default FicusPollAdapter 46 | -------------------------------------------------------------------------------- /src/ficus/simple-poll/index.js: -------------------------------------------------------------------------------- 1 | import SimplePoll from './simple-poll' 2 | import './simple-poll.scss' 3 | 4 | export default SimplePoll 5 | -------------------------------------------------------------------------------- /src/ficus/simple-poll/simple-poll.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import _ from 'lodash' 3 | 4 | const safeFraction = (a, b) => (!b ? 0.0 : a / b) 5 | 6 | const BarToolip = ({ votes, percent }) => ( 7 |
8 |
9 | {votes || 0} · {(percent || 0).toFixed(1)}% 10 |
11 |
12 | ) 13 | 14 | const Bar = ({ width, height, bar }) => { 15 | return ( 16 |
17 | 18 | 19 |
26 |
27 | ) 28 | } 29 | 30 | class SimplePoll extends Component { 31 | getBarHeight(value, max) { 32 | const step = 10 33 | 34 | const currentLimit = step * Math.ceil(max / step) 35 | return Math.max(1, 100.0 * safeFraction(value, currentLimit)) 36 | } 37 | 38 | render() { 39 | const subHeader = `${this.getVotersCount()} votes on` 40 | 41 | const { poll } = this.props 42 | 43 | const bars = this.getChartData() 44 | 45 | const maxVotes = _.max(bars.map(b => b.votes)) 46 | const barWidth = 100.0 / bars.length 47 | 48 | return ( 49 |
50 |
51 |
52 | {subHeader} 53 | {poll.url} 54 |
55 | 56 |
{this.getVotersCount()}
57 |
58 | 59 |
{poll.title}
60 | 61 |
62 |
63 | {bars.map(bar => ( 64 | 70 | ))} 71 |
72 | 73 |
74 | {bars.map(bar => ( 75 |
80 |
{bar.label}
81 |
82 | ))} 83 |
84 |
85 |
86 | ) 87 | } 88 | 89 | getVotersCount() { 90 | return this.props.poll.votersCount 91 | } 92 | 93 | getChartData() { 94 | const { poll } = this.props 95 | if (!poll) return [] 96 | 97 | return poll.choices.map(choice => { 98 | let result = poll.results[choice.id] 99 | 100 | return { 101 | ...choice, 102 | label: choice.text, 103 | weight: result.percent, 104 | votes: result.votes 105 | } 106 | }) 107 | } 108 | } 109 | 110 | export default SimplePoll 111 | -------------------------------------------------------------------------------- /src/ficus/simple-poll/simple-poll.scss: -------------------------------------------------------------------------------- 1 | .simple-poll__bar-tooltip-inner { 2 | background-color: #222; 3 | color: white; 4 | 5 | font-size: 22px; 6 | font-weight: bold; 7 | padding: 6px 10px; 8 | 9 | border-radius: 4px; 10 | } 11 | 12 | .simple-poll__total { 13 | display: inline-block; 14 | 15 | border: 2px solid black; 16 | border-radius: 4px; 17 | 18 | color: black; 19 | font-weight: bold; 20 | font-size: 32px; 21 | line-height: 36px; 22 | 23 | border-bottom-left-radius: 0; 24 | border-top-left-radius: 0; 25 | 26 | height: 36px; 27 | padding: 8px 16px; 28 | } 29 | 30 | .simple-poll__subheader-inner { 31 | display: inline-flex; 32 | align-items: center; 33 | 34 | background-color: black; 35 | padding: 6px 14px; 36 | border-radius: 4px; 37 | 38 | 39 | border-bottom-right-radius: 0; 40 | border-top-right-radius: 0; 41 | } 42 | 43 | .simple-poll__bar-tooltip { 44 | padding-bottom: 8px; 45 | } 46 | 47 | 48 | .simple-poll { 49 | width: 1024px; 50 | height: 768px; 51 | box-sizing: border-box; 52 | display: flex; 53 | flex-flow: column nowrap; 54 | align-items: stretch; 55 | font-family: Helvetica; 56 | 57 | padding: 15px; 58 | } 59 | 60 | .simple-poll__header { 61 | padding-top: 20px; 62 | padding-bottom: 15px; 63 | font-size: 42px; 64 | text-align: center; 65 | flex: 0 0 auto; 66 | } 67 | 68 | .simple-poll__subheader { 69 | font-size: 24px; 70 | text-align: center; 71 | padding-top: 4px; 72 | font-weight: 300; 73 | flex: 0 0 auto; 74 | color: #eee; 75 | text-align: center; 76 | 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | } 81 | 82 | .simple-poll__subheader-link { 83 | color: white; 84 | font-size: 38px; 85 | text-decoration: underline; 86 | font-weight: bold; 87 | margin-left: 12px; 88 | } 89 | 90 | .simple-poll__poll-chart { 91 | flex: 1 0; 92 | width: 100%; 93 | } 94 | 95 | .simple-poll__votes { 96 | height: 100%; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: stretch; 100 | align-items: stretch; 101 | padding: 20px 0; 102 | } 103 | 104 | .simple-poll__bars { 105 | flex-grow: 1; 106 | flex-shrink: 1; 107 | display: flex; 108 | flex-direction: row; 109 | justify-content: center; 110 | } 111 | 112 | .simple-poll__labels { 113 | flex-shrink: 0; 114 | display: flex; 115 | flex-direction: row; 116 | justify-content: center; 117 | } 118 | 119 | .simple-poll__label-col, 120 | .simple-poll__bar { 121 | max-width: 180px; 122 | box-sizing: border-box; 123 | } 124 | 125 | .simple-poll__bar { 126 | display: flex; 127 | flex-direction: column; 128 | justify-content: flex-end; 129 | align-items: center; 130 | padding: 0 5px; 131 | } 132 | 133 | .simple-poll__bar-progress { 134 | border-radius: 5px; 135 | transition: height 0.3s ease; 136 | width: 100%; 137 | } 138 | 139 | .simple-poll__label { 140 | color: '#444'; 141 | font-size: 18px; 142 | text-align: center; 143 | padding: 5px; 144 | font-weight: bold; 145 | } 146 | 147 | .simple-poll__label-col { 148 | flex-shrink: 0; 149 | padding: 0 5px; 150 | padding-bottom: 20px; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/presentation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | // Presentation pieces 5 | import { Presentation, Slide } from 'presa' 6 | import { Title, Caption, Code, H2, H3, H4 } from 'presa/blocks' 7 | import FigureCaption from 'blocks/figure-caption' 8 | 9 | // Interactive slides 10 | import TransistorSlide from 'slides/transistor' 11 | import { CloudPollSlide, PollsSlide, BubblePollSlide } from 'slides/poll-slides' 12 | import MotionGhostSlide from 'slides/motion-ghost-slide' 13 | import { TalkingHeads } from 'slides/actuation/talking-heads' 14 | import ControlledRenderSlide from 'slides/own-render' 15 | import { 16 | TimelineComparisonSlide, 17 | RafScheduleSlide, 18 | RafTimestampSlide, 19 | RafDeltaSlide 20 | } from 'slides/raf-vs-timeout' 21 | import { 22 | CssTransitionCodeSlide, 23 | ReactMotionCodeSlide 24 | } from 'slides/transitions' 25 | import DialogSlide from 'slides/enter-exit' 26 | import FlipSlide from 'slides/flip' 27 | import GameSlide from 'slides/boids' 28 | import ResourcesSlide from 'slides/etc/resources' 29 | import SummarySlide from 'slides/etc/summary' 30 | import LinksSlide from 'slides/etc/links' 31 | import { 32 | AnimationsExpectationsSlide, 33 | AnimationsRealitySlide 34 | } from 'slides/etc/animation-expectations' 35 | 36 | import { GoldenRuleSlide, ReverseRuleSlide } from 'slides/etc/golden-rule' 37 | 38 | const DirtyAnimations = props => ( 39 | 47 | 52 | 53 | Animations ↝
in a stateful world 54 |
55 | 56 | 57 | 58 | Alexey Taktarov
React Kyiv March 59 |
60 | 61 | @mlfrg · molefrog.com 62 |
63 |
64 | 65 | 66 |

Alexey Taktarov

67 |

68 | Co-founder and team lead at resume.io 69 |

70 |

71 | (before) product designer/dev at Shogun{' '} 72 | YC'17 73 |

74 |
75 | 76 | 77 | 78 | 79 | ~500k 80 | registered users 81 | 82 | 1.5 years 83 | active, since 2016 84 | 85 | 6 countries 86 | US and Europe 87 | 88 |
89 | 90 | 91 |
92 | 93 | 94 |

talk's topic

95 |

Animation Patterns in React apps

96 |
97 | 98 | 99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 110 | 111 | 112 | 116 | 117 | 118 | 119 | 120 | {`const redraw = _ => { 121 | points.forEach(point => { 122 | 123 | // make sure \`will-change: transform\` is set 124 | point.element.style.transform = \` 125 | translate3d($\{point.x}px, $\{point.y}px, 0.0px) 126 | rotate($\{point.angle}rad)\` 127 | }) 128 | } 129 | 130 | const tick = ts => { 131 | _lastRaf = requestAnimationFrame(tick) 132 | 133 | physicsStep(delta) 134 | redraw(delta) 135 | }`} 136 | 137 | The requestAnimationFrame-based game skeleton. 138 | 139 | 140 | 141 | {/* Как выглядят современные веб-приложения */} 142 | 143 | 144 | 145 | Immutable UI is the foundation of modern apps.
146 | Application state maps to DOM. 147 |
148 |
149 | 150 | 151 | 152 | 153 | Application as a chain of immutable states. 154 | 155 | 156 | 157 | 158 | 159 | 160 |

161 | Immutable UIs are predictable, maintainable
and better to test. 162 |

163 | 164 | But what about animations? 165 |
166 | 167 | 173 | CSS transitions 174 | are supported out of the box in React 175 | 176 | 177 | 178 | 179 | 180 | 181 | 185 | 186 | 187 | 188 | 189 | 190 |

The downsides of using React Motion

191 | 192 |
  • Spring-based animations are not time limited
  • 193 |
  • Hard to work with sequences
  • 194 |
  • Performance issues
  • 195 |
    196 |
    197 | 198 | 204 | Dirty animations 205 | A dialog window example 206 | 207 | 208 | 209 | {` 210 | class Dialog extends Component { 211 | componentDidMount() { 212 | const node = findDOMNode(this) 213 | 214 | // Or $.animate, anime.js, GSAP, D3 ... 215 | Velocity(node, { scale: 1.5 }, 216 | { duration: 1000 }) 217 | } 218 | 219 | render() { ... } 220 | }`} 221 | 222 | The «animation on enter» pattern. Works through{' '} 223 | componentDidMount lifecycle hook. 224 | 225 | 226 | 227 | 228 | {`class Dialog extends Component { 229 | componentDidMount() { 230 | const node = findDOMNode(this) 231 | 232 | // animate returns a cancellable 233 | // Promise-like object 234 | this._anim = animate(node, { ... }) 235 | } 236 | 237 | componentWillUnmount() { 238 | this._anim && this._anim.cancel() 239 | } 240 | }`} 241 | 242 | Make sure you stop the animation on unmount. 243 | 244 | 245 | 246 | 247 | 248 | 249 | {` 250 |
    251 | {this.state.showDialog && } 252 |
    253 | `}
    254 | 255 | Animating exit is hard — there is nothing to animate since the component 256 | is not in the DOM anymore. 257 | 258 |
    259 | 260 | 261 | {` 262 | 263 | {this.state.showDialog && } 264 | 265 | `} 266 | 267 | Introducing Animated — a helper component that supports 268 | exit animation 269 | 270 | 271 | 272 | 273 | 274 | 275 | The state map of the Animated component 276 | 277 | 278 | 279 | 280 | {`const element = 281 | 282 | // => { type: Dialog, props: { size: 'medium' }, ... } 283 | const element = React.createElement(Dialog, { size: 'medium' })`} 284 | 285 | 286 | JSX is nothing but a light-weight serializable object. 287 | 288 | 289 | 290 | 294 | 295 | 296 | {` 297 | componentWillReceiveProps(nextProps) { 298 | // Exit transition 299 | if (this.props.children && !nextProps.children) { 300 | return this.transitionState(st.EXITING, 301 | { children: this.props.children }) 302 | } 303 | } 304 | 305 | transitionState(transitionTo, opt = {}) { 306 | // .. FSM logic .. 307 | // Wait for \`this._content.animateExit()\` 308 | } 309 | `} 310 | 311 | Animated component implementation:
    312 | Finite State Machine. 313 |
    314 |
    315 | 316 | 320 | 321 | 322 | {`import Transition 323 | from 'react-transition-group/Transition' 324 | 325 | // \`state\` is 'entered', 'entering', 'exited' etc. 326 | 327 | {state => 328 | } 329 | 330 | `} 331 | 332 | 333 | react-transition-group@2.0 provides
    a declarative way 334 | for enter/exit animations 335 |
    336 |
    337 | 338 | 339 | 340 | 341 | {`render() { 342 | return 343 | } 344 | 345 | // Render only once! 346 | shouldComponentUpdate() { return false } 347 | 348 | componentWillReceiveProps(nextProps) { 349 | if (this.props.color != nextProps.color) { 350 | // Animate on canvas... 351 | } 352 | }`} 353 | 354 | Using lifecycle hooks you can completely{' '} 355 | override component's render. For exaple when using{' '} 356 | Canvas, WebGL, WebAudio etc. 357 | 358 | 359 | 360 | 361 | breaking changes in React 16.3: 362 | componentWillReceiveProps() 363 | static getDerivedStateFromProps() 364 |

    365 | but it can only be used to devide state from props,
    366 | so use componentDidUpdate instead. 367 |

    368 |
    369 | 370 | 371 | 372 | 373 | {`// Limit delta to avoid divergence 374 | const delta = Math.min(100.0, ts - prevTs) 375 | const P = 0.001 * delta 376 | 377 | this.x += P * (this.target - x)`} 378 | 379 | 380 | This little trick called P-controller (taken from control theory) 381 | is handy for time-unbound animations 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | {`import { Actuator, actuate } from 'redux-actuator' 395 | 396 | // Inside the component 397 | 398 | 399 | // Where the business logic is 400 | store.dispatch(actuate('animateBadge')) 401 | store.dispatch(actuate('highlightUser', { id: 1 }))`} 402 | 403 | 404 | Actuator is used in Redux apps where global state is the only way to 405 | communicate. 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 |

    React Native

    417 |

    418 | Building high-performance animations
    using Animated API 419 |

    420 |
    421 | 422 | 423 |
    424 | 425 | 426 | 427 | {`import { Animated } from 'react-native' 428 | 429 | // inside a constructor 430 | const animValue = new Animated.Value(0) 431 | this.state = { animValue }`} 432 | 433 | 434 | Step 1. Define a value. 435 | 436 | 437 | 438 | 439 | 440 | {`const { animValue } = this.state 441 | 442 | `} 454 | 455 | 456 | Step 2. Connect it to the View. 457 | 458 | 459 | 460 | 461 | 462 | {` 463 | // or \`spring\`, \`decay\` etc. 464 | Animated.timing(animValue, { 465 | toValue: 0, 466 | duration: 500 467 | }).start() 468 | `} 469 | 470 | 471 | Step 3. Fire it up! 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 |
    481 | ) 482 | 483 | export default DirtyAnimations 484 | 485 | const MetricNumber = styled.div` 486 | font-size: 38px; 487 | font-weight: 700; 488 | ` 489 | 490 | const MetricLabel = styled.div` 491 | text-transform: uppercase; 492 | font-size: 17px; 493 | font-weight: 600; 494 | color: #a0a0a0; 495 | letter-spacing: 0.5px; 496 | margin-bottom: 20px; 497 | ` 498 | 499 | const ImmutableLogo = styled.img` 500 | margin-bottom: 24px; 501 | ` 502 | 503 | const Question = styled.div` 504 | margin-top: 20px; 505 | font-size: 25px; 506 | background-color: #fbf177; 507 | padding: 2px 3px; 508 | ` 509 | 510 | const ConsList = styled.ul` 511 | font-size: 26px; 512 | text-align: left; 513 | padding: 0; 514 | padding-top: 15px; 515 | color: #333333; 516 | 517 | li { 518 | margin-bottom: 32px; 519 | list-style-type: none; 520 | 521 | &:before { 522 | content: '❌'; 523 | padding-right: 15px; 524 | } 525 | } 526 | ` 527 | 528 | const OutlineCode = styled.code` 529 | font-size: 26px; 530 | margin: 15px; 531 | 532 | ${props => props.strikethrough && 'text-decoration: line-through;'}; 533 | ` 534 | 535 | const CustomImageLayout = styled.div` 536 | width: 100%; 537 | height: 100%; 538 | 539 | display: flex; 540 | flex-direction: column; 541 | align-items: center; 542 | justify-content: center; 543 | 544 | > img { 545 | align-self: stretch; 546 | } 547 | ` 548 | 549 | const ResumeioLogo = styled.img` 550 | margin-bottom: 38px; 551 | ` 552 | 553 | const YCBadge = styled.span` 554 | background: #f06530; 555 | padding: 3px 5px; 556 | color: white; 557 | font-weight: 700; 558 | border-radius: 3px; 559 | margin: 0 2px; 560 | ` 561 | 562 | const DeckTitle = styled(Title)` 563 | line-height: 0.95; 564 | margin-top: 90px; 565 | ` 566 | 567 | const Contacts = styled.div` 568 | display: flex; 569 | justify-content: space-between; 570 | margin-top: 180px; 571 | color: white; 572 | font-weight: bold; 573 | font-size: 24px; 574 | align-items: flex-end; 575 | ` 576 | 577 | const Author = styled.div` 578 | border-bottom: 4px solid white; 579 | padding-top: 10px; 580 | ` 581 | 582 | const TwitterHandle = styled.div` 583 | font-size: 20px; 584 | ` 585 | -------------------------------------------------------------------------------- /src/slides/actuation/action-logger.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import { Actuator } from 'redux-actuator' 4 | import { TransitionMotion, spring, presets } from 'react-motion' 5 | 6 | // Action logger event entry box 7 | const ActionLoggerEvent = ({ msg, alpha }) => ( 8 | 16 | 17 | {msg.key} 18 | 19 | 20 | 21 | ch: 22 | {msg.channel} 23 | 24 | 25 | 26 | arg: 27 | {JSON.stringify(msg.args)} 28 | 29 | 30 | ) 31 | 32 | class Logger extends Component { 33 | constructor(props) { 34 | super(props) 35 | this.state = { logs: [] } 36 | } 37 | 38 | render() { 39 | const { logs } = this.state 40 | 41 | // styles for TransitionMotion 42 | const motionStyles = logs.map((msg, i) => ({ 43 | key: msg.timestamp.toString(), 44 | data: msg, 45 | style: { 46 | x: spring(1.0, presets.wobbly) 47 | } 48 | })) 49 | 50 | return ( 51 | 52 | {!!logs.length && Action Logger} 53 | 54 | this.setState({ logs: [msg, ...logs] }) 57 | }} 58 | > 59 | ({ x: spring(0) })} 61 | willEnter={() => ({ x: 0 })} 62 | styles={motionStyles} 63 | > 64 | {interpolatedStyles => ( 65 | 66 | {interpolatedStyles.map(config => { 67 | const msg = config.data 68 | const { x } = config.style 69 | 70 | return ( 71 | 72 | ) 73 | })} 74 | 75 | )} 76 | 77 | 78 | 79 | ) 80 | } 81 | } 82 | 83 | const ActionLogger = styled.div` 84 | align-self: stretch; 85 | ` 86 | 87 | const Events = styled.div` 88 | white-space: nowrap; 89 | ` 90 | 91 | const LoggerLabel = styled.div` 92 | font-size: 18px; 93 | padding-left: 16px; 94 | margin-bottom: 4px; 95 | color: #757575; 96 | font-weight: 500; 97 | ` 98 | 99 | const Field = styled.span` 100 | font-weight: bold; 101 | ` 102 | 103 | const Line = styled.div` 104 | white-space: nowrap; 105 | overflow: hidden; 106 | text-overflow: ellipsis; 107 | ` 108 | 109 | const Entry = styled.div` 110 | display: inline-block; 111 | background-color: #ffffe6; 112 | border: 2px solid #ddd; 113 | border-radius: 6px; 114 | padding: 8px 14px; 115 | margin: 4px; 116 | font-size: 14px; 117 | font-family: 'SF Mono', 'Lucida Console', Monaco, monospace; 118 | ` 119 | 120 | export default Logger 121 | -------------------------------------------------------------------------------- /src/slides/actuation/talking-heads.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import _ from 'lodash' 4 | 5 | // Redux swiss knife 6 | import { createStore, combineReducers } from 'redux' 7 | import { Provider } from 'react-redux' 8 | import createEngine, { actuate, Actuator } from 'redux-actuator' 9 | 10 | // Local components 11 | import ThisGuy from './this-guy' 12 | import ActionLogger from './action-logger' 13 | import Button from 'blocks/button' 14 | 15 | // This will create store. The store is instatiated inside 16 | // components' constructors which isn't cool, but works for demos 17 | const configureStore = () => { 18 | const engine = createEngine() 19 | 20 | const rootReducer = combineReducers({ actuator: engine.reducer }) 21 | return createStore(rootReducer) 22 | } 23 | 24 | // This is a wrapper around ThisGuys component with an Actuator 25 | // Normally the Actuator is placed inside the component, but 26 | // I wanted to isolate the component from Redux-related logic 27 | class TalkingGuy extends Component { 28 | render() { 29 | const { channel = 'default', ...restProps } = this.props 30 | 31 | return ( 32 | { 35 | const guy = this.guy 36 | guy && guy.say(...args) 37 | }} 38 | > 39 | { 41 | this.guy = g 42 | }} 43 | {...restProps} 44 | /> 45 | 46 | ) 47 | } 48 | } 49 | 50 | const internationalPhrases = { 51 | jose: ['¡Hóla!', 'Cómo estás?', 'Puedes repetirlo?', 'Lo siento, pero...'], 52 | 53 | gustav: ['Goeden avond!', 'Goeiedag!', 'Dank u wel'], 54 | 55 | pierrot: [ 56 | 'Quoi de neuf?', 57 | 'Bonjour!', 58 | 'Salut!', 59 | 'Coucou!', 60 | 'Au revoir!', 61 | 'Comment ça va?' 62 | ], 63 | 64 | avram: ['Tzohorayim Tovim', 'Lilah Tov', 'Shabbat Shalom', 'Shavua Tov'] 65 | } 66 | 67 | const logActuatorAction = (store, action, color = null) => { 68 | store.dispatch(action) 69 | store.dispatch( 70 | actuate('log', { 71 | color: color, 72 | ...action.payload 73 | }) 74 | ) 75 | } 76 | 77 | // First demo. One guy talking on demand. 78 | // Only one Actuator channel 79 | class TalkingHeads extends Component { 80 | constructor() { 81 | super() 82 | this.state = { 83 | isMany: false 84 | } 85 | this.store = configureStore() 86 | } 87 | 88 | saySomething() { 89 | const guy = this.state.isMany 90 | ? _.sample(Object.keys(internationalPhrases)) 91 | : 'gustav' 92 | 93 | const phrase = _.sample(internationalPhrases[guy]) 94 | 95 | const actuatorAction = actuate(guy, phrase) 96 | 97 | const colors = { 98 | jose: 'rgba(247, 49, 111, 0.2)', 99 | gustav: null, 100 | pierrot: 'rgba(64, 148, 237, 0.2)', 101 | avram: 'rgba(42, 213, 139, 0.2)' 102 | } 103 | logActuatorAction(this.store, actuatorAction, colors[guy]) 104 | } 105 | 106 | render() { 107 | const store = this.store 108 | 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | Gustav 117 | 118 | 119 | {this.state.isMany && ( 120 | 121 | 122 | 123 | José 124 | 125 | )} 126 | 127 | {this.state.isMany && ( 128 | 129 | 130 | 131 | Pierrot 132 | 133 | )} 134 | 135 | {this.state.isMany && ( 136 | 137 | 138 | 139 | Avram 140 | 141 | )} 142 | 143 | 144 | 145 | 148 | 149 | 155 | 156 | 157 | 158 | 159 | 160 | ) 161 | } 162 | } 163 | 164 | const Container = styled.div` 165 | display: flex; 166 | flex-flow: column nowrap; 167 | align-items: center; 168 | justify-content: center; 169 | padding-top: 90px; 170 | ` 171 | 172 | const Heads = styled.div` 173 | display: flex; 174 | flex-flow: row nowrap; 175 | justify-content: center; 176 | 177 | > div { 178 | margin: 0 30px; 179 | } 180 | ` 181 | 182 | const Head = styled.div`` 183 | 184 | const HeadName = styled.div` 185 | font-size: 18px; 186 | text-align: center; 187 | margin-top: 12px; 188 | font-weight: 500; 189 | 190 | &.head-jose { 191 | color: #666; 192 | } 193 | 194 | &.head-gustav { 195 | color: #f7316f; 196 | } 197 | 198 | &.head-pierrot { 199 | color: #4094ed; 200 | } 201 | 202 | &.head-avram { 203 | color: #2ad58b; 204 | } 205 | ` 206 | 207 | const Controls = styled.div` 208 | margin-top: 22px; 209 | margin-bottom: 16px; 210 | ` 211 | 212 | export { TalkingHeads } 213 | -------------------------------------------------------------------------------- /src/slides/actuation/this-guy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node */ 2 | import Velocity from 'velocity-animate' 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import { findDOMNode } from 'react-dom' 6 | import { Howl } from 'howler' 7 | 8 | class ThisGuy extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | this.whoopSound = new Howl({ src: './sounds/whoop.mp3', volume: 0.6 }) 12 | } 13 | 14 | render() { 15 | // Default viewport 16 | const viewport = { width: 70, height: 110 } 17 | 18 | // Scaling 19 | const factor = this.props.zoomFactor 20 | const width = viewport.width * factor 21 | const height = viewport.height * factor 22 | 23 | const imageUrl = `url(./images/guys/${this.props.who}.svg)` 24 | 25 | return ( 26 | 33 | ) 34 | } 35 | 36 | doMagic(phrase) { 37 | const container = findDOMNode(this) 38 | 39 | Velocity(container, { scaleY: 0.1 }, { duration: 100 }) 40 | Velocity(container, { scaleY: 1.0 }, { duration: 800, easing: [30, 5] }) 41 | } 42 | 43 | say(phrase) { 44 | const container = findDOMNode(this) 45 | if (!container) return 46 | 47 | const dialog = document.createElement('div') 48 | dialog.classList.add('guy__dialog') 49 | 50 | const dialogText = document.createElement('div') 51 | dialogText.classList.add('guy__dialog-text') 52 | dialogText.textContent = phrase 53 | 54 | dialog.appendChild(dialogText) 55 | container.appendChild(dialog) 56 | 57 | dialogText.style.fontSize = `${this.props.zoomFactor * 12}px` 58 | 59 | Velocity( 60 | dialogText, 61 | { scaleX: 0.3, scaleY: 0.3, opacity: 0.3 }, 62 | { duration: 0 } 63 | ) 64 | 65 | Velocity( 66 | dialogText, 67 | { 68 | scaleX: '1.0', 69 | scaleY: '1.0' 70 | }, 71 | { duration: 800, easing: [250, 15], queue: false } 72 | ) 73 | 74 | Velocity( 75 | dialogText, 76 | { opacity: 1.0 }, 77 | { duration: 200, easing: 'easeOutCirc', queue: false } 78 | ) 79 | 80 | Velocity( 81 | dialogText, 82 | { translateY: '-400px' }, 83 | { 84 | duration: 6000, 85 | queue: false, 86 | complete: () => container.removeChild(dialog) 87 | } 88 | ) 89 | 90 | setTimeout(() => { 91 | if (!findDOMNode(this)) return 92 | Velocity(dialogText, { opacity: 0.0 }, { duration: 1000, queue: false }) 93 | }, 2000) 94 | 95 | this.whoopSound.play() 96 | } 97 | } 98 | 99 | ThisGuy.defaultProps = { 100 | zoomFactor: 1.0, 101 | who: 'tikhon' 102 | } 103 | 104 | const Img = styled.div` 105 | cursor: pointer; 106 | position: relative; 107 | 108 | width: 70px; 109 | height: 110px; 110 | 111 | background-repeat: no-repeat; 112 | background-size: 100% auto; 113 | background-position: bottom; 114 | 115 | .guy__dialog { 116 | position: absolute; 117 | bottom: 85%; 118 | left: 50%; 119 | transform: translate(-50%, 0); 120 | } 121 | 122 | .guy__dialog-text { 123 | background-color: black; 124 | color: white; 125 | padding: 3px 10px; 126 | border-radius: 3px; 127 | font-size: 16px; 128 | text-align: center; 129 | font-family: Rubik, sans-serif; 130 | } 131 | ` 132 | 133 | export default ThisGuy 134 | -------------------------------------------------------------------------------- /src/slides/boids/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Slide } from 'presa' 4 | 5 | import Simulator from './simulator' 6 | 7 | // transforms event's mouse coordinates to relative ones 8 | // e.g. [0.3, 0.9] 9 | const toRelativeMouseCoords = event => { 10 | const rect = event.currentTarget.getBoundingClientRect() 11 | 12 | return { 13 | x: (event.clientX - rect.left) / rect.width, 14 | y: (event.clientY - rect.top) / rect.height 15 | } 16 | } 17 | 18 | class GameSlide extends React.Component { 19 | constructor(props) { 20 | super(props) 21 | 22 | this.state = { 23 | started: false 24 | } 25 | } 26 | 27 | handleKeyDown = event => { 28 | // A 29 | if (event.keyCode === 65) { 30 | this._engine.alphaLaw = !this._engine.alphaLaw 31 | } 32 | 33 | // S 34 | if (event.keyCode === 83) { 35 | this._engine.betaLaw = !this._engine.betaLaw 36 | } 37 | 38 | // D 39 | if (event.keyCode === 68) { 40 | this._engine.gammaLaw = !this._engine.gammaLaw 41 | } 42 | 43 | // F 44 | if (event.keyCode === 70) { 45 | this._engine.attractLaw = !this._engine.attractLaw 46 | } 47 | } 48 | 49 | componentDidMount() { 50 | this._engine = new Simulator(this._root) 51 | document.body.addEventListener('keydown', this.handleKeyDown) 52 | } 53 | 54 | componentWillUnmount() { 55 | this._engine.stop() 56 | document.body.removeEventListener('keydown', this.handleKeyDown) 57 | } 58 | 59 | handleMouseMove = event => { 60 | const point = toRelativeMouseCoords(event) 61 | 62 | this._engine.targetX = point.x 63 | this._engine.targetY = point.y 64 | } 65 | 66 | startTheParty = event => { 67 | this.setState({ started: true }) 68 | 69 | const point = toRelativeMouseCoords(event) 70 | this._engine.start(point.x, point.y) 71 | } 72 | 73 | render() { 74 | const { started } = this.state 75 | 76 | return ( 77 | 78 | 79 | (this._root = e)} /> 80 | 84 | 85 | Click anywhere on a screen 86 | 87 | 88 | 89 | 90 | ) 91 | } 92 | } 93 | 94 | const ClickAnywhere = styled.div` 95 | font-weight: 600; 96 | color: #777; 97 | pointer-events: none; 98 | user-select: none; 99 | transition: all 1s ease; 100 | 101 | ${props => 102 | props.invisible && 103 | ` 104 | opacity: 0; 105 | transform: scale(1.3, 1.3) translateY(-100px); 106 | `}; 107 | ` 108 | 109 | const Controls = styled.div` 110 | position: absolute; 111 | cursor: pointer; 112 | width: 100%; 113 | height: 100%; 114 | top: 0; 115 | left: 0; 116 | 117 | display: flex; 118 | align-items: center; 119 | justify-content: center; 120 | ` 121 | 122 | const Container = styled.div` 123 | width: 100%; 124 | height: 100%; 125 | overflow: hidden; 126 | position: relative; 127 | ` 128 | 129 | const Game = styled.div` 130 | width: 100%; 131 | height: 100%; 132 | position: relative; 133 | will-change: transform; 134 | 135 | .boids__fps { 136 | position: absolute; 137 | font-family: monospace; 138 | font-size: 20px; 139 | bottom: 20px; 140 | left: 30px; 141 | 142 | white-space: pre; 143 | } 144 | 145 | .boids__cross { 146 | width: 24px; 147 | height: 24px; 148 | line-height: 24px; 149 | 150 | top: -12px; 151 | left: -12px; 152 | 153 | box-sizing: border-box; 154 | display: inline-block; 155 | position: absolute; 156 | border: 1px solid black; 157 | 158 | font-size: 20px; 159 | text-align: center; 160 | 161 | will-change: transform; 162 | transform: translate3d(-30px, -30px, 0); 163 | 164 | &.is-killed { 165 | opacity: 0.2; 166 | } 167 | } 168 | ` 169 | 170 | export default GameSlide 171 | -------------------------------------------------------------------------------- /src/slides/boids/neighbour-detector.js: -------------------------------------------------------------------------------- 1 | import { quadtree } from 'd3-quadtree' 2 | 3 | export default class NeighbourDetector { 4 | constructor(points) { 5 | this.points = points 6 | } 7 | 8 | detect(x, y, radius) { 9 | let qtree = quadtree(this.points, p => p.x, p => p.y) 10 | return this.quadNeighbours(qtree, x, y, radius) 11 | } 12 | 13 | /* 14 | * Find the nodes within the specified 15 | * circle 16 | * Adapted from http://bl.ocks.org/llb4ll/8709363 17 | */ 18 | quadNeighbours(quadTree, cx, cy, r) { 19 | const x0 = cx - r, 20 | y0 = cy - r, 21 | x3 = cx + r, 22 | y3 = cy + r 23 | 24 | let result = [] 25 | quadTree.visit((node, x1, y1, x2, y2) => { 26 | if (!node.length) { 27 | var d = Math.sqrt( 28 | (node.data.x - cx) * (node.data.x - cx) + 29 | (node.data.y - cy) * (node.data.y - cy) 30 | ) 31 | 32 | if (d <= r) { 33 | result.push(node.data) 34 | } 35 | } 36 | 37 | return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0 38 | }) 39 | 40 | return result 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/slides/boids/simulator.js: -------------------------------------------------------------------------------- 1 | import requestAnimationFrame from 'raf' 2 | import NeighbourDetector from './neighbour-detector' 3 | import { Howl } from 'howler' 4 | 5 | // boid algorithm params 6 | const alpha = 0.006 7 | const beta = 0.09 8 | const gamma = -0.1 9 | 10 | // friction 11 | const theta = -0.03 12 | const alphaRadius = 100.0 13 | const betaRadius = 100.0 14 | const gammaRadius = 30.0 15 | const attraction = 0.001 16 | const explosionBounce = 3000.0 17 | 18 | const boidSymbols = ['✕', '△', '◯', '▧'] 19 | 20 | // Calculates the distance between two points 21 | const distance = (x1, y1, x2, y2) => 22 | Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) 23 | 24 | // Same as _.sample 25 | const pickRandom = array => array[Math.floor(Math.random() * array.length)] 26 | 27 | const startSound = new Howl({ src: './sounds/game-start.ogg' }) 28 | // const slapSound = new Howl({ src: './sounds/slap.ogg' }) 29 | 30 | class Simulator { 31 | constructor(_root, N = 100) { 32 | this._root = _root 33 | this.width = 0 34 | this.height = 0 35 | 36 | this.targetX = 0.0 37 | this.targetY = 0.0 38 | 39 | this.alphaLaw = true 40 | this.betaLaw = true 41 | this.gammaLaw = true 42 | this.attractLaw = false 43 | 44 | this.boids = Array(N) 45 | .fill(0) 46 | .map((_, i) => ({ 47 | idx: i, 48 | active: true, 49 | x: 0, 50 | y: 0, 51 | vx: 0, 52 | vy: 0, 53 | symbol: pickRandom(boidSymbols) 54 | })) 55 | 56 | this.boidElements = this.boids.map(boid => { 57 | const element = document.createElement('div') 58 | 59 | element.classList.add('boids__cross') 60 | element.textContent = boid.symbol 61 | 62 | // append to the container element 63 | _root.appendChild(element) 64 | 65 | return element 66 | }) 67 | 68 | this.fpsCounter = document.createElement('div') 69 | this.fpsCounter.classList.add('boids__fps') 70 | _root.appendChild(this.fpsCounter) 71 | } 72 | 73 | start(initX = 0.5, initY = 0.5) { 74 | // Restart 75 | this.stop() 76 | this.isStopped = false 77 | 78 | this.width = this._root.offsetWidth 79 | this.height = this._root.offsetHeight 80 | 81 | this.boids.forEach(b => { 82 | b.x = this.width * initX 83 | b.y = this.height * initY 84 | b.vx = Math.random() * 3.0 - 1.5 85 | b.vy = Math.random() * 3.0 - 1.5 86 | }) 87 | 88 | startSound.play() 89 | this._lastRaf = requestAnimationFrame(this.tick) 90 | } 91 | 92 | stop() { 93 | this.isStopped = true 94 | requestAnimationFrame.cancel(this._lastRaf) 95 | } 96 | 97 | redraw(delta) { 98 | const fps = delta ? 1000.0 / delta : 0.0 99 | this.fpsCounter.textContent = 100 | `delta: ${delta.toFixed(6)} fps: ${fps.toFixed(6)}\n` + 101 | `game: theseguys.io` 102 | 103 | for (var i = 0; i < this.boids.length; ++i) { 104 | const b = this.boids[i] 105 | const angle = Math.atan2(b.vy, b.vx) 106 | 107 | // Update element's transform style 108 | const element = this.boidElements[i] 109 | 110 | element.style.transform = ` 111 | translate3d(${b.x}px, ${b.y}px, 0.0px) 112 | rotate(${angle}rad)` 113 | } 114 | } 115 | 116 | tick = ts => { 117 | if (this.isStopped) return 118 | 119 | this._lastRaf = requestAnimationFrame(this.tick) 120 | 121 | // calculate delta 122 | const prevTs = this.prevTs || ts 123 | const delta = Math.min(100.0, ts - prevTs) || 0.0001 124 | this.prevTs = ts 125 | 126 | this.doPhysics(delta) 127 | this.redraw(delta) 128 | } 129 | 130 | doPhysics(delta) { 131 | const detector = new NeighbourDetector(this.boids) 132 | 133 | this.boids.forEach((b, i) => { 134 | if (!b.active) return 135 | 136 | let cx = 0.0 137 | let cy = 0.0 138 | let cvx = 0.0 139 | let cvy = 0.0 140 | 141 | let collisionx = 0.0 142 | let collisiony = 0.0 143 | 144 | let n1 = 0 145 | let n2 = 0 146 | 147 | let nearestBoids = detector.detect(b.x, b.y, 100) 148 | 149 | nearestBoids.forEach((ob, j) => { 150 | if (i === j || !ob.active) { 151 | return 152 | } 153 | 154 | let d = Math.sqrt( 155 | (ob.x - b.x) * (ob.x - b.x) + (ob.y - b.y) * (ob.y - b.y) 156 | ) 157 | 158 | if (d < alphaRadius) { 159 | cx += ob.x 160 | cy += ob.y 161 | n1 += 1 162 | } 163 | 164 | if (d < betaRadius) { 165 | cvx += ob.vx 166 | cvy += ob.vy 167 | n2 += 1 168 | } 169 | 170 | if (d < gammaRadius) { 171 | collisionx += ob.x - b.x 172 | collisiony += ob.y - b.y 173 | } 174 | }) 175 | 176 | cx = n1 ? cx / n1 : 0.0 177 | cy = n1 ? cy / n1 : 0.0 178 | cvx = n2 ? cvx / n2 : 0.0 179 | cvy = n2 ? cvy / n2 : 0.0 180 | 181 | const _alpha = this.alphaLaw ? alpha : 0.0 182 | const _beta = this.betaLaw ? beta : 0.0 183 | const _gamma = this.gammaLaw ? gamma : 0.0 184 | 185 | let vx1 = (cx - b.x) * _alpha 186 | let vy1 = (cy - b.y) * _alpha 187 | 188 | let vx2 = (cvx - b.vx) * _beta 189 | let vy2 = (cvy - b.vy) * _beta 190 | 191 | let vx3 = collisionx * _gamma 192 | let vy3 = collisiony * _gamma 193 | 194 | const attrC = this.attractLaw ? attraction : 0.0 195 | 196 | b.vx += 197 | vx1 + 198 | vx2 + 199 | vx3 + 200 | theta * b.vx + 201 | attrC * (this.width * this.targetX - b.x) 202 | b.vy += 203 | vy1 + 204 | vy2 + 205 | vy3 + 206 | theta * b.vy + 207 | attrC * (this.height * this.targetY - b.y) 208 | 209 | if (this.explode) { 210 | const [eX, eY] = this.explode 211 | 212 | const r = distance(eX, eY, b.x, b.y) || 0.01 213 | 214 | b.vx += explosionBounce * (b.x - eX) / (r * r) 215 | b.vy += explosionBounce * (b.y - eY) / (r * r) 216 | } 217 | 218 | b.x += b.vx 219 | b.y += b.vy 220 | 221 | if (b.x <= 0.0) { 222 | b.x = 0.0 223 | b.vx = -1.0 * b.vx 224 | } 225 | 226 | if (b.x >= this.width) { 227 | b.x = this.width 228 | b.vx = -1.0 * b.vx 229 | } 230 | 231 | if (b.y <= 0.0) { 232 | b.y = 0.0 233 | b.vy = -1.0 * b.vy 234 | } 235 | 236 | if (b.y >= this.height) { 237 | b.y = this.height 238 | b.vy = -1.0 * b.vy 239 | } 240 | }) 241 | 242 | this.explode = null 243 | } 244 | } 245 | 246 | export default Simulator 247 | -------------------------------------------------------------------------------- /src/slides/enter-exit/01-dialog-enter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node */ 2 | import React, { Component } from 'react' 3 | import { findDOMNode } from 'react-dom' 4 | 5 | import Velocity from 'velocity-animate' 6 | 7 | import FakeDialog from './fake-dialog' 8 | 9 | class AppearableDialog extends Component { 10 | componentDidMount() { 11 | const { slow } = this.props 12 | const root = findDOMNode(this) 13 | 14 | if (!root) { 15 | return 16 | } 17 | 18 | const duration = slow ? 2000 : 600 19 | 20 | Velocity(root, { scale: 0.3, opacity: 0 }, { duration: 0 }) 21 | Velocity( 22 | root, 23 | { scale: 1.0, opacity: 1 }, 24 | { duration: duration, easing: [55, 7] } 25 | ) 26 | } 27 | 28 | render() { 29 | return 30 | } 31 | } 32 | 33 | class DialogEnter extends Component { 34 | render() { 35 | return this.props.active && 36 | } 37 | } 38 | 39 | export default DialogEnter 40 | -------------------------------------------------------------------------------- /src/slides/enter-exit/02-dialog-exit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node */ 2 | import React, { Component } from 'react' 3 | import { findDOMNode } from 'react-dom' 4 | 5 | import Velocity from 'velocity-animate' 6 | 7 | import FakeDialog from './fake-dialog' 8 | import Animated from './animated' 9 | 10 | /* 11 | * 12 | */ 13 | class Dialog extends Component { 14 | animateAppear() { 15 | const { slow } = this.props 16 | const node = findDOMNode(this) 17 | 18 | if (!node) { 19 | return 20 | } 21 | 22 | const duration = slow ? 2000 : 600 23 | 24 | Velocity(node, { scale: 0.3, opacity: 0, translateY: 0 }, { duration: 0 }) 25 | let promise = Velocity( 26 | node, 27 | { scale: 1.0, opacity: 1 }, 28 | { duration: duration, easing: [55, 7] } 29 | ) 30 | 31 | promise.cancel = () => Velocity(node, 'stop') 32 | return promise 33 | } 34 | 35 | animateExit() { 36 | const { slow } = this.props 37 | const node = findDOMNode(this) 38 | 39 | if (!node) { 40 | return 41 | } 42 | 43 | let promise = Velocity( 44 | node, 45 | { translateY: 100, opacity: 0.0 }, 46 | { duration: slow ? 2000 : 1000, easing: 'easeOutCubic' } 47 | ) 48 | 49 | promise.cancel = () => Velocity(node, 'stop') 50 | return promise 51 | } 52 | 53 | render() { 54 | return 55 | } 56 | } 57 | 58 | // Showcase 59 | class DialogExit extends Component { 60 | render() { 61 | return ( 62 | 63 | {this.props.active && } 64 | 65 | ) 66 | } 67 | } 68 | 69 | export default DialogExit 70 | -------------------------------------------------------------------------------- /src/slides/enter-exit/animated.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-find-dom-node */ 2 | import React, { Component } from 'react' 3 | 4 | const animationStates = { 5 | IDLE: 'IDLE', 6 | ENTERING: 'ENTERING', 7 | ENTERED: 'ENTERED', 8 | EXITING: 'EXITING', 9 | EXITED: 'EXITED' 10 | } 11 | 12 | const st = animationStates 13 | 14 | class Animated extends Component { 15 | constructor(props) { 16 | super(props) 17 | 18 | this._anims = [] 19 | 20 | this.state = { 21 | ghostChildren: props.children, 22 | animationState: st.EXITED 23 | } 24 | } 25 | 26 | // Dummiest animation manager ever. 27 | // Simply saves animation handles and cancels 28 | // them all on demand. 29 | registerAnimation(descriptor) { 30 | descriptor && this._anims.push(descriptor) 31 | } 32 | 33 | cancelAllAnimations() { 34 | this._anims.forEach(anim => anim.cancel()) 35 | this._anims.splice(0, this._anims.length) 36 | } 37 | 38 | componentWillUnmount() { 39 | this.cancelAllAnimations() 40 | this._unmounted = true 41 | } 42 | 43 | changeAnimationState(state, cb) { 44 | const stateName = state.animationState 45 | 46 | if (stateName) { 47 | this.props.onAnimationState(stateName) 48 | } 49 | 50 | return this.setState(state, cb) 51 | } 52 | 53 | transitionState(transitionTo, options = {}) { 54 | const transitionFrom = this.state.animationState 55 | 56 | // Do nothing 57 | if (transitionFrom === transitionTo) { 58 | return 59 | } 60 | 61 | // terminal states 62 | if (transitionTo === st.ENTERED) { 63 | return this.changeAnimationState({ 64 | animationState: st.ENTERED 65 | }) 66 | } 67 | 68 | if (transitionTo === st.EXITED) { 69 | return this.changeAnimationState({ 70 | animationState: st.EXITED 71 | }) 72 | } 73 | 74 | if (transitionTo === st.ENTERING) { 75 | return this.changeAnimationState( 76 | { 77 | animationState: st.ENTERING 78 | }, 79 | () => { 80 | this.cancelAllAnimations() 81 | 82 | if (!this.$content || !this.$content.animateAppear) { 83 | return this.transitionState(st.ENTERED) 84 | } 85 | 86 | const animation = this.$content.animateAppear() 87 | 88 | animation.then(() => { 89 | if (!this._unmounted && this.state.animationState !== st.EXITING) { 90 | this.transitionState(st.ENTERED) 91 | } 92 | }) 93 | this.registerAnimation(animation) 94 | } 95 | ) 96 | } 97 | 98 | if (transitionTo === st.EXITING) { 99 | return this.changeAnimationState( 100 | { 101 | animationState: st.EXITING, 102 | ghostChildren: options.children 103 | }, 104 | () => { 105 | this.cancelAllAnimations() 106 | 107 | if (!this.$content || !this.$content.animateExit) { 108 | return this.transitionState(st.EXITED) 109 | } 110 | 111 | const animation = this.$content.animateExit() 112 | animation.then(() => { 113 | if (!this._unmounted && this.state.animationState !== st.ENTERING) { 114 | this.transitionState(st.EXITED) 115 | } 116 | }) 117 | this.registerAnimation(animation) 118 | } 119 | ) 120 | } 121 | } 122 | 123 | componentWillReceiveProps(nextProps) { 124 | if (this.props.children && !nextProps.children) { 125 | return this.transitionState(st.EXITING, { children: this.props.children }) 126 | } 127 | 128 | if (!this.props.children && nextProps.children) { 129 | return this.transitionState(st.ENTERING) 130 | } 131 | } 132 | 133 | componentDidMount() { 134 | this.transitionState(st.ENTERING) 135 | } 136 | 137 | registerElement = node => (this.$content = node) 138 | 139 | render() { 140 | const { animationState, ghostChildren } = this.state 141 | 142 | // This flags shows if we have to render 'ghost' 143 | // version of the children prop. 144 | // Only works when exit animation is performing 145 | const shouldRenderGhost = animationState === st.EXITING 146 | const childrenToRender = shouldRenderGhost 147 | ? ghostChildren 148 | : this.props.children 149 | 150 | // Nothing to render yet 151 | if (!childrenToRender) { 152 | return null 153 | } 154 | 155 | return React.cloneElement(childrenToRender, { ref: this.registerElement }) 156 | } 157 | } 158 | 159 | export default Animated 160 | -------------------------------------------------------------------------------- /src/slides/enter-exit/fake-dialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | class FakeDialog extends Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 56 | 57 | 64 | 65 | {hasMonitor && ( 66 | 73 | )} 74 |
    75 | 76 | 77 | 78 | 83 | 84 | 85 | {hasMonitor && 86 | this.state.showMonitor && ( 87 | 88 | 89 | 90 | )} 91 | 92 | 93 | 94 | ) 95 | } 96 | } 97 | 98 | const SlideLayout = styled.div` 99 | width: 100%; 100 | height: 100%; 101 | display: flex; 102 | flex-flow: column; 103 | align-items: center; 104 | ` 105 | 106 | const DialogPreview = styled.div` 107 | transform: scale(1.1, 1.1); 108 | padding-top: 60px; 109 | padding-bottom: 60px; 110 | pointer-events: none; 111 | 112 | display: flex; 113 | align-items: center; 114 | justify-content: center; 115 | 116 | height: 290px; 117 | width: 400px; 118 | ` 119 | 120 | const Showcase = styled.div` 121 | display: flex; 122 | align-items: center; 123 | margin-top: 60px; 124 | ` 125 | 126 | const Monitor = styled.div` 127 | width: 300px; 128 | margin-left: 60px; 129 | padding-left: 60px; 130 | border-left: 2px solid rgba(0, 0, 0, 0.02); 131 | ` 132 | 133 | export default HooksSlide 134 | -------------------------------------------------------------------------------- /src/slides/enter-exit/state-monitor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | import colors from 'colors' 4 | 5 | const StateEntry = props => ( 6 | 7 | 8 | 9 | {props.name} 10 | 11 | 12 | ) 13 | 14 | class StateMonitor extends Component { 15 | render() { 16 | return ( 17 |
    18 | 23 | 28 | 33 | 38 |
    39 | ) 40 | } 41 | } 42 | 43 | const StateName = styled.div` 44 | margin-left: 18px; 45 | font-size: 26px; 46 | ` 47 | 48 | const StateEntryContainer = styled.div` 49 | margin-bottom: 18px; 50 | display: flex; 51 | align-items: center; 52 | 53 | transition: opacity 0.2s ease; 54 | opacity: ${props => (props.active ? 1.0 : 0.2)}; 55 | ` 56 | 57 | const StateDot = styled.div` 58 | width: 28px; 59 | height: 28px; 60 | 61 | background-color: ${props => props.color}; 62 | border-radius: 28px; 63 | ` 64 | 65 | export default StateMonitor 66 | -------------------------------------------------------------------------------- /src/slides/etc/animation-expectations.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Slide } from 'presa' 4 | 5 | export const AnimationsExpectationsSlide = () => ( 6 | 7 | 8 | 9 |
    Ideal Animation
    10 | 11 | runs smoothly within
    equal periods of time 12 |
    13 |
    14 | 15 | 16 | 17 | 18 |
    19 |
    20 | ) 21 | 22 | export const AnimationsRealitySlide = () => ( 23 | 24 | 25 | 26 |
    setTimeout
    27 | 28 | not guaranteed to be executed in given time interval 29 | 30 |
    31 | 32 | 33 | 34 | 35 |
    36 | 37 | 38 | 39 |
    requestAnimationFrame
    40 | 41 | schedule a call before the next browser repaint 42 | 43 |
    44 | 45 | 46 | 47 | 48 |
    49 |
    50 | ) 51 | 52 | const Column = styled.div` 53 | display: flex; 54 | width: 100%; 55 | align-items: center; 56 | margin: 20px; 57 | ` 58 | 59 | const Row = styled.div` 60 | width: 50%; 61 | padding: 20px 35px; 62 | text-align: left; 63 | 64 | &:first-child { 65 | text-align: right; 66 | } 67 | ` 68 | 69 | const Header = styled.div` 70 | font-size: 28px; 71 | margin-bottom: 8px; 72 | ` 73 | 74 | const Description = styled.div` 75 | font-size: 24px; 76 | color: #656565; 77 | ` 78 | -------------------------------------------------------------------------------- /src/slides/etc/golden-rule.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Slide } from 'presa' 4 | 5 | export const GoldenRuleSlide = () => ( 6 | 7 | 8 | 9 | 10 | Keep the component's interfrace
    as pure as possible and 11 | make sure
    its side-effects don't break the contract. 12 |
    13 |
    14 | ) 15 | 16 | export const ReverseRuleSlide = () => ( 17 | 18 | 19 | 20 | 21 | You can use only pure state changes 22 |
    in order to trigger animations 23 |
    24 | 25 | 26 | 27 | the traditional alternative is to use refs or PubSub 28 |
    29 | ) 30 | 31 | const Label = styled.div` 32 | text-transform: uppercase; 33 | font-weight: 600; 34 | color: #a08447; 35 | font-size: 18px; 36 | letter-spacing: 0.2px; 37 | margin-bottom: 20px; 38 | ` 39 | 40 | const Rule = styled.div` 41 | font-size: 28px; 42 | line-height: 1.4; 43 | margin-bottom: 28px; 44 | ` 45 | 46 | const Comment = styled.div` 47 | margin-top: 32px; 48 | font-size: 22px; 49 | color: #808080; 50 | ` 51 | -------------------------------------------------------------------------------- /src/slides/etc/links.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import { Slide, BuiltWithPresa } from 'presa' 4 | 5 | const LinksSlide = () => ( 6 | 7 | link to the slides 8 | 9 | 👉 10 | 14 | molefrog.com/stateful-animations 15 | 16 | 17 | source code 18 | 19 | ⭐️ 20 | 24 | github.com/molefrog/stateful-animations 25 | 26 | 27 | slides made with 28 | 29 | 🔥 30 | 31 | github.com/molefrog/presa 32 | 33 | 34 | 50 | 51 | ) 52 | 53 | export default LinksSlide 54 | 55 | const Footer = styled.div` 56 | display: flex; 57 | align-items: center; 58 | justify-content: space-between; 59 | margin-top: 140px; 60 | margin-bottom: 10px; 61 | ` 62 | 63 | const OwnContacts = styled.div` 64 | font-size: 22px; 65 | font-weight: 500; 66 | &, 67 | a { 68 | color: #3f5ffb; 69 | } 70 | ` 71 | 72 | const LinkWrap = styled.div` 73 | display: flex; 74 | margin-top: 4px; 75 | margin-bottom: 32px; 76 | ` 77 | 78 | const LinkIcon = styled.div` 79 | font-size: 36px; 80 | margin-right: 10px; 81 | ` 82 | 83 | const FinalLink = styled.a` 84 | font-size: 32px; 85 | font-weight: 500; 86 | text-decoration: underline; 87 | color: #333; 88 | padding: 0 8px; 89 | transition: all 0.2s ease; 90 | 91 | &:hover { 92 | background: #333; 93 | color: white; 94 | } 95 | ` 96 | -------------------------------------------------------------------------------- /src/slides/etc/resources.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Slide } from 'presa' 3 | import styled from 'styled-components' 4 | 5 | const Video = ({ thumb, url, title, author }) => ( 6 | 7 | 8 | {title} 9 | {author} 10 | 11 | ) 12 | 13 | class ResourcesSlide extends React.Component { 14 | render() { 15 | return ( 16 | 17 | 18 | 56 | 57 | ) 58 | } 59 | } 60 | 61 | const VideoGrid = styled.div` 62 | display: flex; 63 | flex-flow: row wrap; 64 | ` 65 | 66 | const VideoAuthor = styled.div` 67 | font-size: 17px; 68 | ` 69 | 70 | const VideoName = styled.div` 71 | font-size: 18px; 72 | font-weight: 500; 73 | margin-bottom: 2px; 74 | ` 75 | 76 | const VideoThumb = styled.div` 77 | width: 260px; 78 | height: 170px; 79 | 80 | margin-bottom: 10px; 81 | 82 | background-size: cover; 83 | background-position: top left; 84 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 85 | 0px 0px 8px rgba(0, 0, 0, 0.05); 86 | 87 | border-radius: 4px; 88 | ` 89 | 90 | const VideoItem = styled.a` 91 | margin: 0 11px; 92 | margin-bottom: 42px; 93 | text-decoration: none; 94 | color: inherit; 95 | 96 | &:hover { 97 | ${VideoThumb} { 98 | box-shadow: 0px 0px 0px 3px #3f5ffb; 99 | } 100 | } 101 | ` 102 | 103 | export default ResourcesSlide 104 | -------------------------------------------------------------------------------- /src/slides/etc/summary.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Slide } from 'presa' 3 | import styled from 'styled-components' 4 | 5 | class SummarySlide extends React.Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 |
    12 | Pure animations 13 | 14 | Only state changes allowed!{' '} 15 | 16 | CSS transitions, react-motion,
    react-transition-group, 17 | React Move, ... 18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 |
    26 | Dirty animations 27 | 28 | They use lifecycle hooks and might manipulate DOM directly
    29 | Web Animations, Velocity, GSAP, D3, anime.js, ... 30 |
    31 |
    32 |
    33 | 34 | 35 | 36 |
    37 | Own render 38 | 39 | Use this when you need to work with external APIs like WebGL, 40 | Canvas. 41 | 42 |
    43 |
    44 | 45 | 46 | 47 |
    48 | Complex cases 49 | Inter-component communication, FLIP method. 50 |
    51 |
    52 |
    53 | ) 54 | } 55 | } 56 | 57 | const MethodDesc = styled.div` 58 | line-height: 1.35; 59 | ` 60 | 61 | const MethodName = styled.div` 62 | font-size: 26px; 63 | margin-bottom: 10px; 64 | font-weight: 500; 65 | ` 66 | 67 | const Icon = styled.img` 68 | margin-right: 32px; 69 | flex-shrink: 0; 70 | ` 71 | 72 | const Line = styled.div` 73 | display: flex; 74 | margin-bottom: 38px; 75 | 76 | ${props => 77 | props.extra && 78 | ` 79 | border-top: 2px solid #ddd; 80 | padding-top: 25px; 81 | `}; 82 | ` 83 | 84 | export default SummarySlide 85 | -------------------------------------------------------------------------------- /src/slides/flip/animated-route.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | class Route extends Component { 5 | getLayerOpacity() { 6 | const { debug, animationState } = this.props 7 | 8 | // for debug purposes 9 | const semiTransparent = 0.3 10 | 11 | switch (animationState) { 12 | case 'entering': 13 | return debug ? semiTransparent : 0.0 14 | case 'entered': 15 | return 1.0 16 | 17 | case 'exiting': 18 | return 1.0 19 | 20 | case 'exited': 21 | return 0.0 22 | } 23 | } 24 | render() { 25 | const { debug, isList, animationState } = this.props 26 | 27 | const isAbsolute = 28 | animationState === 'exiting' || animationState === 'exited' 29 | 30 | // for debug mode 31 | const layerOffset = -30 + -75 * (isList ? -1 : 0) 32 | const debugBackground = isList 33 | ? 'rgba(233, 30, 99, 0.2)' 34 | : 'rgba(33, 150, 243, 0.2)' 35 | 36 | return ( 37 | 43 | 44 | position: {isAbsolute ? 'absolute' : 'static'} 45 | 46 | 47 | 52 | {this.props.children} 53 | 54 | 55 | ) 56 | } 57 | } 58 | 59 | const Content = styled.div` 60 | width: 100%; 61 | height: 100%; 62 | transition: ${props => (props.debug ? 'opacity 0.3s ease' : 'none')}; 63 | border-radius: 5px; 64 | background: rgba(0, 0, 0, 0); 65 | padding: 25px 30px; 66 | opacity: ${props => props.opacity}; 67 | ` 68 | 69 | const DebugInfo = styled.div` 70 | position: absolute; 71 | left: 60%; 72 | top: 100%; 73 | width: 300px; 74 | color: #333; 75 | 76 | padding: 14px 0; 77 | font-size: 24px; 78 | font-weight: 500; 79 | transition: opacity 0.3s ease; 80 | 81 | opacity: ${props => (props.visible ? 1 : 0)}; 82 | font-family: ${props => props.theme.slide.monoFont}; 83 | ` 84 | 85 | const Container = styled.div` 86 | width: 100%; 87 | height: 100%; 88 | 89 | * { 90 | box-sizing: border-box; 91 | } 92 | 93 | transition: border-color 0.6s ease, transform 0.6s ease; 94 | will-change: transform; 95 | border: 2.5px solid transparent; 96 | border-radius: 6px; 97 | border-color: #efefef; 98 | 99 | // 3d perspective 100 | ${props => 101 | props.debug && 102 | ` 103 | transform: rotateX(-60deg) rotateZ(-40deg) translateZ(${props.zOffset}px); 104 | border-color: ${props.debugBackground}; 105 | `}; 106 | 107 | ${props => 108 | props.isAbsolute && 109 | ` 110 | position: absolute; 111 | top: 0; 112 | left: 0; 113 | `}; 114 | ` 115 | 116 | export default Route 117 | -------------------------------------------------------------------------------- /src/slides/flip/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Slide } from 'presa' 3 | import styled from 'styled-components' 4 | 5 | // Animation utils 6 | import Transition from 'react-transition-group/Transition' 7 | import Velocity from 'velocity-animate' 8 | 9 | import Route from './animated-route' 10 | import Button from 'blocks/button' 11 | 12 | // getBoundingClientRect will not work properly with transforms 13 | // (in demo mode only!), so we use this replacement. 14 | // 15 | // Change it to `getBoundingClientRect` when using in your project. 16 | const getBoundingClientRect = el => { 17 | return { 18 | top: el.offsetTop, 19 | left: el.offsetLeft, 20 | width: el.offsetWidth, 21 | height: el.offsetHeight 22 | } 23 | } 24 | 25 | class FlipSlide extends React.Component { 26 | constructor(props) { 27 | super(props) 28 | 29 | this.state = { 30 | isList: true, 31 | debug: false 32 | } 33 | } 34 | 35 | getDuration() { 36 | // make it slower in debug mode 37 | return this.state.debug ? 1500 : 500 38 | } 39 | 40 | switchRoute = () => { 41 | this.setState(state => ({ 42 | isList: !state.isList 43 | })) 44 | } 45 | 46 | // Perform FLIP animation of a card 47 | animateFlip = inTransition => { 48 | const source = inTransition ? this._smallCard : this._bigCard 49 | const target = inTransition ? this._bigCard : this._smallCard 50 | 51 | const sR = getBoundingClientRect(source) 52 | const tR = getBoundingClientRect(target) 53 | 54 | Velocity( 55 | source, 56 | { 57 | translateX: tR.left - sR.left, 58 | translateY: tR.top - sR.top, 59 | scale: tR.width / sR.width 60 | }, 61 | { duration: this.getDuration(), easing: 'easeInOutQuart' } 62 | ) 63 | 64 | // Return the element back the original state. 65 | // Only need this line because both layers stay in DOM 66 | // during the demo. Consider this a hack. 67 | Velocity( 68 | source, 69 | { 70 | translateX: 0, 71 | translateY: 0, 72 | scale: 1 73 | }, 74 | { duration: 0, delay: 350 } 75 | ) 76 | } 77 | 78 | render() { 79 | const duration = this.getDuration() 80 | 81 | return ( 82 | 83 | 84 | 91 | 92 | 93 | 94 | this.animateFlip(true)} 98 | > 99 | {transState => { 100 | const hideRest = transState !== 'entered' 101 | 102 | return ( 103 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | (this._smallCard = el)} 117 | /> 118 | 119 | 120 | 121 | ) 122 | }} 123 | 124 | 125 | this.animateFlip(false)} 129 | > 130 | {transState => { 131 | const hideRest = transState !== 'entered' 132 | 133 | return ( 134 | 139 | 140 | { 142 | el && (this._bigCard = el) 143 | }} 144 | /> 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | ) 156 | }} 157 | 158 | 159 | 160 | ) 161 | } 162 | } 163 | 164 | const SingleView = styled.div` 165 | display: flex; 166 | ` 167 | 168 | const BigPreview = styled.div` 169 | transform-origin: top left; 170 | width: 260px; 171 | height: 300px; 172 | 173 | background: #b6e6ff; 174 | border-radius: 12px; 175 | ` 176 | 177 | const FakeText = styled.div` 178 | transition: opacity ${props => props.duration * 0.5 / 1000.0}s ease; 179 | opacity: ${props => (props.transparent ? 0 : 1)}; 180 | padding-top: 26px; 181 | ` 182 | 183 | const FakeTextLine = styled.div` 184 | width: 180px; 185 | height: 26px; 186 | background: rgba(42, 108, 159, 0.05); 187 | border-radius: 3px; 188 | 189 | margin-bottom: 18px; 190 | margin-left: 32px; 191 | ` 192 | 193 | const List = styled.div` 194 | display: grid; 195 | padding-top: 20px; 196 | 197 | grid-column-gap: 20px; 198 | grid-row-gap: 24px; 199 | justify-content: center; 200 | 201 | grid-template-columns: repeat(auto-fill, 130px); 202 | grid-auto-flow: dense; 203 | ` 204 | 205 | const ListItem = styled.div` 206 | transform-origin: top left; 207 | width: 130px; 208 | height: 150px; 209 | 210 | border-radius: 6px; 211 | will-change: transform; 212 | 213 | transition: opacity ${props => props.duration * 0.5 / 1000.0}s ease; 214 | opacity: ${props => (props.transparent ? 0 : 1)}; 215 | 216 | &:nth-child(1) { 217 | background: #e5e9f2; 218 | } 219 | 220 | &:nth-child(2) { 221 | background: #ffe8e6; 222 | } 223 | 224 | &:nth-child(3) { 225 | background: #fff4d6; 226 | } 227 | 228 | &:nth-child(4) { 229 | background: #c3ffc6; 230 | } 231 | 232 | &:nth-child(5) { 233 | background: #b6e6ff; 234 | } 235 | 236 | &:nth-child(6) { 237 | background: #f3e7ff; 238 | } 239 | ` 240 | 241 | const Controls = styled.div` 242 | display: flex; 243 | align-items: center; 244 | margin-bottom: 26px; 245 | ` 246 | 247 | const WorkArea = styled.div` 248 | width: 540px; 249 | height: 420px; 250 | 251 | position: relative; 252 | cursor: pointer; 253 | 254 | perspective: 0px; 255 | transition: transform 0.5s ease; 256 | ` 257 | 258 | export default FlipSlide 259 | -------------------------------------------------------------------------------- /src/slides/golden-rule/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/molefrog/stateful-animations/e237ff128cb9b113ade862c147dcfe2686499576/src/slides/golden-rule/index.js -------------------------------------------------------------------------------- /src/slides/motion-ghost-slide.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Motion, TransitionMotion, spring, presets } from 'react-motion' 3 | 4 | import { Slide } from 'presa' 5 | import Button from 'blocks/button' 6 | 7 | // Utils 8 | const makeKey = (t, i) => `${t}-${i}` 9 | const nextTick = fn => setTimeout(fn, 0) 10 | 11 | /* 12 | * UI-related options: colors, sizes etc. 13 | */ 14 | const options = { 15 | boxColor: '#F62466', 16 | timelineColor: '#CFCFCF', 17 | pointColor: '#2AD58B', 18 | pointSelectedColor: '#F62466', 19 | edgeColor: '#4094ED', 20 | 21 | width: 800, 22 | height: 400, 23 | timelineWidth: 500 24 | } 25 | 26 | /* 27 | * Box entity component 28 | */ 29 | const Entity = ({ t, opacity, width, radius = 50 }) => ( 30 | 38 | 47 | 48 | ) 49 | 50 | class MotionGhost extends Component { 51 | constructor() { 52 | super() 53 | this.state = { 54 | alpha: 0.0, 55 | ghosts: [], 56 | currentTime: null, 57 | isWobble: false 58 | } 59 | 60 | this.handleMouseMove = this.handleMouseMove.bind(this) 61 | this.handleMouseLeave = this.handleMouseLeave.bind(this) 62 | this.switchCorners = this.switchCorners.bind(this) 63 | } 64 | 65 | switchCorners() { 66 | this.setState({ 67 | alpha: this.state.alpha ? 0.0 : 1.0, 68 | ghosts: [] 69 | }) 70 | } 71 | 72 | componentWillUnmount() { 73 | clearTimeout(this._lastTick) 74 | } 75 | 76 | componentDidMount() { 77 | if (!this.state.ghosts.length) { 78 | this.switchCorners() 79 | } 80 | } 81 | 82 | handleMouseMove(event) { 83 | const { left, width } = event.currentTarget.getBoundingClientRect() 84 | 85 | const x = (event.pageX - left) / width 86 | 87 | const padRatio = 88 | 0.5 * (options.width - options.timelineWidth) / options.width 89 | const tx = Math.max(0, x - padRatio) / (1.0 - 2 * padRatio) 90 | 91 | const sorted = [...this.state.ghosts, 1.0, 0.0].sort( 92 | (a, b) => Math.abs(a - tx) - Math.abs(b - tx) 93 | ) 94 | 95 | const current = sorted[0] 96 | 97 | if (this.state.currentTime !== current) { 98 | this.setState({ currentTime: current }) 99 | } 100 | } 101 | 102 | handleMouseLeave() { 103 | this.setState({ currentTime: null }) 104 | } 105 | 106 | render() { 107 | const controlPoints = 30 108 | const { width, height, timelineWidth } = options 109 | const { currentTime, alpha, isWobble } = this.state 110 | 111 | const springParams = isWobble 112 | ? { stiffness: 100, damping: 10 } 113 | : { stiffness: 120, damping: 30 } 114 | 115 | const timelineMargins = 20 116 | 117 | return ( 118 | 119 | 127 | {/* Render box and its ghost copies */} 128 | 129 | 133 | {value => { 134 | const t = value.x 135 | 136 | // This will fill up `ghosts` array during the animation 137 | this._lastTick = nextTick(() => { 138 | const { ghosts, alpha } = this.state 139 | const step = 1.0 / controlPoints 140 | 141 | if ( 142 | (alpha && t > step * ghosts.length) || 143 | (!alpha && t < 1.0 - step * ghosts.length) 144 | ) { 145 | this.setState({ ghosts: [...ghosts, t] }) 146 | } 147 | }) 148 | 149 | return 150 | }} 151 | 152 | 153 | {/* Ghosts */} 154 | {this.state.ghosts.map((t, i) => ( 155 | 161 | ))} 162 | 163 | 164 | {/* Timeline */} 165 | 166 | 177 | 178 | {/* Render control ghost points */} 179 | ({ x: spring(0) })} 181 | willEnter={() => ({ x: 0 })} 182 | styles={this.state.ghosts.map((t, i) => ({ 183 | key: makeKey(t, i), 184 | data: t, 185 | style: { 186 | x: spring(t === currentTime ? 1.4 : 1.0, presets.stiff) 187 | } 188 | }))} 189 | > 190 | {interpolatedStyles => ( 191 | 192 | {interpolatedStyles.map(config => { 193 | const t = config.data 194 | const { x } = config.style 195 | 196 | return ( 197 | 209 | ) 210 | })} 211 | 212 | )} 213 | 214 | 215 | {/* 0 and 1 control points */} 216 | 226 | 236 | 237 | {/* Current state label */} 238 | {currentTime && ( 239 | 243 | {value => ( 244 | 245 | {`{ x: ${currentTime.toFixed(6)} }`} 246 | 247 | )} 248 | 249 | )} 250 | 251 | 252 | 253 |
    254 | 257 | 258 | 265 |
    266 |
    267 | ) 268 | } 269 | } 270 | 271 | export default MotionGhost 272 | -------------------------------------------------------------------------------- /src/slides/own-render/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Slide } from 'presa' 3 | import styled from 'styled-components' 4 | 5 | import Rotator from './rotator' 6 | import DynamicCode from 'blocks/dynamic-code' 7 | import Button from 'blocks/button' 8 | import { Code } from 'presa/blocks' 9 | 10 | const demoBg = '#dfe5f3' 11 | 12 | const RotatorCode = ({ mouseX, mouseY, rotX, rotY }) => ( 13 | 14 | {' 16 | {'} rotY={'} 17 | 18 | {'} />\n\n'} 19 | 20 | ) 21 | 22 | const InnerCode = ({ mouseX, mouseY, rotX, rotY }) => ( 23 | 24 | {"// Rotator's internal state:\nthis.x = "} 25 | 26 | {'\nthis.y = '} 27 | 28 | {'\n'} 29 | 30 | ) 31 | 32 | class WebglSlide extends React.Component { 33 | constructor(props) { 34 | super(props) 35 | 36 | this.state = { 37 | isControlled: false, 38 | mouseX: 0.0, 39 | mouseY: 0.0, 40 | rotX: 0.0, 41 | rotY: 0.0 42 | } 43 | } 44 | 45 | handleMouseMove = event => { 46 | const rect = event.currentTarget.getBoundingClientRect() 47 | 48 | this.setState({ 49 | mouseX: (event.clientX - rect.left) / rect.width, 50 | mouseY: (event.clientY - rect.top) / rect.height 51 | }) 52 | } 53 | 54 | handleTick = (x, y) => { 55 | this.setState({ 56 | rotX: x, 57 | rotY: y 58 | }) 59 | } 60 | 61 | render() { 62 | return ( 63 | 64 | 65 | 66 | 67 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {!this.state.isControlled && ( 85 |
    86 |
    Update internal state immediately on props change:
    87 | {`this.x = props.rotX`} 88 |
    89 | )} 90 | 91 | {this.state.isControlled && ( 92 |
    93 |
    Using P-controller filter to animate smoothly:
    94 | {`this.x += P * (props.rotX - this.x)`} 95 |
    96 | )} 97 |
    98 |
    99 | 100 | 101 | 108 | 109 |
    110 |
    111 | ) 112 | } 113 | } 114 | 115 | const Extra = styled.div` 116 | border-top: 2px solid #eee; 117 | margin-top: 34px; 118 | padding-top: 34px; 119 | ` 120 | 121 | const Buttons = styled.div` 122 | display: flex; 123 | justify-content: flex-start; 124 | margin-bottom: 28px; 125 | ` 126 | 127 | const Layout = styled.div` 128 | width: 100%; 129 | height: 100%; 130 | display: flex; 131 | ` 132 | 133 | const Demo = styled.div` 134 | flex: 3 0; 135 | background: ${demoBg}; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | ` 140 | 141 | const Side = styled.div` 142 | flex: 2 0; 143 | padding: 2em 2.5em; 144 | ` 145 | 146 | export default WebglSlide 147 | -------------------------------------------------------------------------------- /src/slides/own-render/rotator.js: -------------------------------------------------------------------------------- 1 | import requestAnimationFrame from 'raf' 2 | import React from 'react' 3 | 4 | import { 5 | Scene, 6 | PerspectiveCamera, 7 | WebGLRenderer, 8 | MeshNormalMaterial, 9 | IcosahedronGeometry, 10 | Mesh 11 | } from 'three' 12 | 13 | class Rotator extends React.Component { 14 | static defaultProps = { 15 | size: 500, 16 | background: '#ffffff' 17 | } 18 | 19 | doRender = ts => { 20 | if (this.destroy) { 21 | return 22 | } 23 | 24 | // Schedule next frame 25 | requestAnimationFrame(this.doRender) 26 | 27 | // Calculate frame delta 28 | const prevTs = this.prevTs || ts 29 | this.prevTs = ts 30 | const delta = Math.min(100.0, ts - prevTs) 31 | 32 | const targetX = 4 * Math.PI * (this.mouseX || 0.0) 33 | const targetY = 3 * Math.PI * (this.mouseY || 0.0) 34 | 35 | if (this.props.controlled) { 36 | // P-controller parameter (depends on delta!) 37 | const p = 0.001 * delta 38 | 39 | this.cube.rotation.z += p * (targetX - this.cube.rotation.z) 40 | this.cube.rotation.x += p * (targetY - this.cube.rotation.x) 41 | } else { 42 | this.cube.rotation.z = targetX 43 | this.cube.rotation.x = targetY 44 | } 45 | 46 | this.props.onTick(this.cube.rotation.z, this.cube.rotation.x) 47 | 48 | this.renderer.setClearColor(this.props.background, 1) 49 | this.renderer.render(this.scene, this.camera) 50 | } 51 | 52 | componentDidMount() { 53 | const { size } = this.props 54 | this.scene = new Scene() 55 | this.camera = new PerspectiveCamera(75, size / size, 0.1, 1000) 56 | 57 | // Init WebGL renderer 58 | this.renderer = new WebGLRenderer() 59 | this.renderer.setSize(size, size) 60 | this.renderer.setClearColor(this.props.background, 1) 61 | this._root.appendChild(this.renderer.domElement) 62 | 63 | var geometry = new IcosahedronGeometry(1, 1) 64 | var material = new MeshNormalMaterial({ flatShading: true }) 65 | this.cube = new Mesh(geometry, material) 66 | this.scene.add(this.cube) 67 | this.camera.position.z = 2 68 | 69 | requestAnimationFrame(this.doRender) 70 | } 71 | 72 | componentWillUnmount() { 73 | // Three.js doesn't provide good methods for disposal 74 | this.destroy = true 75 | } 76 | 77 | componentWillReceiveProps(nextProps) { 78 | this.mouseX = nextProps.rotX 79 | this.mouseY = nextProps.rotY 80 | } 81 | 82 | // Do not rerender 83 | shouldComponentUpdate() { 84 | return false 85 | } 86 | 87 | render() { 88 | return ( 89 |
    (this._root = el)} 94 | /> 95 | ) 96 | } 97 | } 98 | 99 | export default Rotator 100 | -------------------------------------------------------------------------------- /src/slides/poll-slides.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import SimplePoll from 'ficus/simple-poll' 4 | import CloudPoll from 'ficus/cloud-poll' 5 | import BubblePoll from 'ficus/bubble-poll' 6 | import ClassicPoll from 'ficus/classic-poll' 7 | 8 | import { Slide } from 'presa' 9 | import Button from 'blocks/button' 10 | 11 | import './poll-slides.scss' 12 | 13 | const makeUid = () => (Math.random() * 0xffffff) | 0 14 | 15 | const initialPoll = { 16 | title: 'What is the best album of 2016?', 17 | url: 'ficus.io', 18 | choices: [ 19 | { 20 | id: 'a1', 21 | color: 'rgba(61, 118, 230, 1.00)', 22 | text: 'Chance The Rapper – Coloring Book' 23 | }, 24 | { id: 'a2', color: 'rgba(241, 196, 15, 1.00)', text: 'Beyoncé – Lemonade' }, 25 | { 26 | id: 'a3', 27 | color: 'rgba(230, 126, 34, 1.00)', 28 | text: 'Frank Ocean – Blonde ' 29 | }, 30 | { id: 'a4', color: 'rgba(231, 76, 60, 1.00)', text: 'David Bowie – Star' }, 31 | { 32 | id: 'a5', 33 | color: 'rgba(142, 68, 173, 1.00)', 34 | text: 'Kanye West – The Life Of Pablo' 35 | } 36 | ], 37 | voters: [] 38 | } 39 | 40 | const makePollPayload = poll => { 41 | const votersCount = poll.voters.length 42 | const results = {} 43 | poll.choices.forEach(ch => { 44 | results[ch.id] = { votes: 0 } 45 | }) 46 | poll.voters.forEach(v => { 47 | results[v.choice].votes++ 48 | }) 49 | 50 | poll.choices.forEach(ch => { 51 | results[ch.id].percent = votersCount 52 | ? results[ch.id].votes / votersCount 53 | : 0 54 | }) 55 | 56 | return { 57 | ...poll, 58 | votersCount, 59 | results 60 | } 61 | } 62 | 63 | const sample = array => array[Math.floor(Math.random() * array.length)] 64 | 65 | const generateVoter = (poll, choices) => { 66 | const ch = choices || poll.choices 67 | 68 | return { 69 | id: makeUid(), 70 | choice: sample(ch).id 71 | } 72 | } 73 | 74 | const addVotes = (poll, amount) => { 75 | const newVoters = Array(amount) 76 | .fill() 77 | .map(() => generateVoter(poll)) 78 | 79 | return { 80 | ...poll, 81 | voters: [...poll.voters, ...newVoters] 82 | } 83 | } 84 | 85 | const changeVotes = (poll, percent) => { 86 | let choices = [] 87 | 88 | for (var i = 0; i < 3; ++i) { 89 | choices.push(sample(poll.choices)) 90 | } 91 | 92 | return { 93 | ...poll, 94 | voters: poll.voters.map(v => ({ 95 | ...v, 96 | choice: Math.random() <= percent ? sample(choices).id : v.choice 97 | })) 98 | } 99 | } 100 | 101 | class PollControls extends Component { 102 | constructor() { 103 | super() 104 | this.state = { 105 | poll: addVotes(initialPoll, 80) 106 | } 107 | } 108 | 109 | changeVotes(percent) { 110 | this.setState({ 111 | poll: changeVotes(this.state.poll, percent) 112 | }) 113 | } 114 | 115 | addVotes(amount = 1) { 116 | this.setState({ 117 | poll: addVotes(this.state.poll, amount) 118 | }) 119 | } 120 | 121 | render() { 122 | return ( 123 |
    124 |
    125 | {this.props.children(makePollPayload(this.state.poll))} 126 |
    127 | 128 |
    129 | 132 | 135 | 138 | 141 |
    142 |
    143 | ) 144 | } 145 | } 146 | 147 | const PollZoom = ({ zoom, children, width, height }) => ( 148 |
    152 |
    156 | {children} 157 |
    158 |
    159 | ) 160 | 161 | export class CloudPollSlide extends Component { 162 | constructor() { 163 | super() 164 | this.state = { alphaTime: 1.4 } 165 | } 166 | 167 | toggleSpeed() { 168 | this.setState({ alphaTime: this.state.alphaTime === 1.4 ? 4.0 : 1.4 }) 169 | } 170 | 171 | render() { 172 | const width = 1100 173 | const height = 660 174 | 175 | const alphaTime = this.state.alphaTime 176 | 177 | return ( 178 | 179 | 180 | {poll => ( 181 | 182 | 189 | 190 | )} 191 | 192 | 193 |
    194 | 201 |
    202 |
    203 | ) 204 | } 205 | } 206 | 207 | export class BubblePollSlide extends Component { 208 | render() { 209 | const width = 1024 210 | const height = 768 211 | 212 | return ( 213 | 214 | 215 | {poll => ( 216 | 217 | 223 | 224 | )} 225 | 226 | 227 | ) 228 | } 229 | } 230 | 231 | export const PollsSlide = props => { 232 | const width = 1024 233 | const height = 768 234 | 235 | return ( 236 | 237 | 238 | {poll => ( 239 |
    240 | 241 | 247 | 248 | 249 | 250 | 256 | 257 | 258 | 259 | 265 | 266 | 267 | 268 | 274 | 275 |
    276 | )} 277 |
    278 |
    279 | ) 280 | } 281 | -------------------------------------------------------------------------------- /src/slides/poll-slides.scss: -------------------------------------------------------------------------------- 1 | .poll-slide, 2 | .poll-slide__wrap { 3 | display: flex; 4 | flex-flow: column nowrap; 5 | justify-content: center; 6 | } 7 | 8 | .poll-slide__controls { 9 | max-width: 75%; 10 | margin: 0 auto; 11 | text-align: center; 12 | 13 | padding: 20px; 14 | padding-bottom: 10px; 15 | 16 | .button { 17 | margin-right: 4px; 18 | margin-bottom: 4px; 19 | } 20 | } 21 | 22 | .poll-slide__speed { 23 | text-align: center; 24 | } 25 | 26 | .poll-slide__content { 27 | display: flex; 28 | flex-flow: row wrap; 29 | justify-content: center; 30 | } 31 | 32 | .poll-slide__polls { 33 | max-width: 80%; 34 | text-align: center; 35 | .poll-zoom__poll { 36 | margin: 4px; 37 | } 38 | } 39 | 40 | .poll-zoom__poll { 41 | overflow: hidden; 42 | border: 1px solid #e1e1e1; 43 | border-radius: 3px; 44 | display: inline-block; 45 | position: relative; 46 | } 47 | 48 | .poll-zoom__poll-wrap { 49 | transform-origin: left top; 50 | } 51 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/comparison-slide.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from 'styled-components' 3 | 4 | import { Slide } from 'presa' 5 | import colors from 'colors' 6 | import TimelineWithMeter from './timeline-with-meter' 7 | 8 | // default discrete step 9 | const step = 0.025 10 | 11 | // sin func but gets from 0 to 1 12 | const timingFunc = t => 0.5 + 0.3 * Math.sin(8 * t) 13 | 14 | class ComparisonSlide extends Component { 15 | constructor() { 16 | super() 17 | this.state = { currentTime: 0.0 } 18 | } 19 | 20 | static defaultProps = { 21 | comparedMethod: 'naive' 22 | } 23 | 24 | handleCursorChange = cursor => { 25 | this.setState({ currentTime: cursor }) 26 | } 27 | 28 | render() { 29 | const numberOfIterations = Math.ceil(1 / step) 30 | 31 | const normalScale = Array(numberOfIterations) 32 | .fill(0) 33 | .map((_, index) => { 34 | return [index * step, 0.0] 35 | }) 36 | 37 | const rafScale = (() => { 38 | let buffer = [] 39 | let x = 0.0 40 | let delay = 0.0 41 | 42 | while (x <= 1.0) { 43 | // carefuly crafted params 44 | const maxDeviation = 0.09 45 | const startFrom = 0.05 46 | 47 | buffer.push([x, delay / maxDeviation]) 48 | 49 | // Add some arificial delay, 50 | // but only after `startFrom`. 51 | const shift = Math.abs(maxDeviation * Math.sin(6 * (x - startFrom))) 52 | delay = x > startFrom ? shift : 0.0 53 | 54 | x += step + delay 55 | } 56 | 57 | return buffer 58 | })() 59 | 60 | const idealGraph = normalScale.map(t => [t[0], timingFunc(t[0])]) 61 | const rafGraph = rafScale 62 | .filter(t => t[0] <= 1) 63 | .map(t => [t[0], timingFunc(t[0]), t[1]]) 64 | 65 | const timeoutGraph = (scale => { 66 | let points = [] 67 | let lastValue = 0.0 68 | 69 | scale.forEach(tt => { 70 | const [t, delay] = tt 71 | points.push([t, timingFunc(lastValue), delay]) 72 | lastValue += step 73 | }) 74 | 75 | return points 76 | })(rafScale) 77 | 78 | // Common props for all timelines 79 | const timelineProps = { 80 | size: 360, 81 | onCursorMove: this.handleCursorChange, 82 | time: this.state.currentTime 83 | } 84 | 85 | const comparedMethod = this.props.comparedMethod 86 | 87 | return ( 88 | 89 | 90 | 91 | 96 | 97 | 98 | Ideal Animation 99 | The time between frames is constant 100 | 101 | 102 | 103 | {comparedMethod === 'raf' && ( 104 | 105 | 110 | 111 | 112 | rAF + delta 113 | 114 | The animation adapts to delta. Could drop frames, but 115 | completes in time! 116 | 117 | 118 | 119 | )} 120 | {comparedMethod === 'naive' && ( 121 | 122 | 127 | 128 | 129 | Naive Implementation 130 | 131 | The step is variable. Animation isn't feasible. 132 | 133 | 134 | 135 | )} 136 | 137 | 138 | ) 139 | } 140 | } 141 | 142 | /* 143 | * Styling and stuff 144 | */ 145 | 146 | const SlideLayout = styled(Slide)` 147 | display: flex; 148 | align-items: center; 149 | ` 150 | 151 | const SlideContent = styled.div` 152 | display: flex; 153 | align-items: flex-start; 154 | justify-content: center; 155 | width: 100%; 156 | ` 157 | 158 | const AnimationMethod = styled.div` 159 | margin: 0 30px; 160 | text-align: center; 161 | ` 162 | 163 | const AnimationDetails = styled.div` 164 | padding: 0 40px; 165 | ` 166 | 167 | const AnimationText = styled.div` 168 | font-size: 20px; 169 | color: ${colors.textGray}; 170 | max-width: 340px; 171 | margin: 0 auto; 172 | line-height: 1.4; 173 | ` 174 | 175 | const AnimationHeader = styled.div` 176 | font-size: 24px; 177 | margin-top: 18px; 178 | margin-bottom: 12px; 179 | ` 180 | 181 | export default ComparisonSlide 182 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/index.js: -------------------------------------------------------------------------------- 1 | import TimelineComparisonSlide from './comparison-slide' 2 | export * from './raf-slides' 3 | 4 | export { TimelineComparisonSlide } 5 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/raf-slides.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { Slide } from 'presa' 4 | import { Code } from 'presa/blocks' 5 | 6 | import FigureCaption from 'blocks/figure-caption' 7 | 8 | // Shows how to use rAF in your code. 9 | export class RafScheduleSlide extends Component { 10 | render() { 11 | return ( 12 | 13 | {`// Or use a polyfill: 14 | // import requestAnimationFrame from 'raf' 15 | const { requestAnimationFrame } = window 16 | 17 | const animate = () => { 18 | requestAnimationFrame(animate) 19 | 20 | // Perform an animation step 21 | x += velocity 22 | } 23 | 24 | // Fire it up 🔥 25 | requestAnimationFrame(animate)`} 26 | 27 | ) 28 | } 29 | } 30 | 31 | export class RafTimestampSlide extends Component { 32 | render() { 33 | return ( 34 | 35 | {`requestAnimationFrame(timestamp => { 36 | // DOMHighResTimeStamp 37 | // timestamp ~> 30485.84100000153 38 | })`} 39 | 40 | rAF passes a high-precision timestamp up to 5 µs{' '} 41 | (microseconds). 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | export class RafDeltaSlide extends Component { 49 | render() { 50 | return ( 51 | 52 | {`const animate = timestamp => { 53 | requestAnimationFrame(animate) 54 | 55 | const delta = timestamp - prevTimestamp 56 | 57 | // Note, it's a function now! 58 | x += velocity(delta) 59 | }`} 60 | 61 | It is important to calculate delta between calls and use it in order 62 | to adapt the animation. 63 | 64 | 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/rolling-meter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styled from 'styled-components' 4 | 5 | const SKINS = { 6 | moon: './images/moon-emoji.png', 7 | sun: './images/sun-emoji.png', 8 | sunglasses: './images/sunglasses-emoji.png' 9 | } 10 | 11 | const Meter = styled.div` 12 | border-radius: 50px; 13 | height: ${props => props.size}px; 14 | ` 15 | 16 | const Ball = styled.div` 17 | height: 52px; 18 | width: 52px; 19 | border-radius: 52px; 20 | 21 | background-image: url(${props => SKINS[props.skin]}); 22 | background-size: contain; 23 | background-position: center; 24 | background-repeat: no-repeat; 25 | 26 | transform: ${props => { 27 | return ` 28 | translateY(${props.size * props.x - 23.0}px) 29 | rotate(${540 * props.t}deg)` 30 | }}; 31 | ` 32 | 33 | class RollingMeter extends React.Component { 34 | static defaultProps = { 35 | skin: 'moon' 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | 47 | export default RollingMeter 48 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/timeline-with-meter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import Timeline from './timeline' 5 | import RollingMeter from './rolling-meter' 6 | 7 | const TMLayout = styled.div` 8 | display: flex; 9 | flex-flow: row nowrap; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | > div { 14 | margin-left: 12px; 15 | } 16 | ` 17 | 18 | const TimelineWithMeter = props => { 19 | const { size, ...rest } = props 20 | const { points, time } = rest 21 | 22 | const filtered = points.filter(p => p[0] <= time) 23 | 24 | // find the current value 25 | const currentPoint = filtered.length 26 | ? filtered[filtered.length - 1] 27 | : [0.0, 0.0] 28 | 29 | return ( 30 | 31 | 32 | 38 | 39 | ) 40 | } 41 | 42 | export default TimelineWithMeter 43 | -------------------------------------------------------------------------------- /src/slides/raf-vs-timeout/timeline.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { interpolateRgb } from 'd3-interpolate' 3 | 4 | import colors from 'colors' 5 | 6 | const interpolateDot = interpolateRgb(colors.green, colors.yellow) 7 | 8 | const Timeline = ({ time, points, onCursorMove, width, height, ...props }) => { 9 | const barsCount = 10 10 | 11 | return ( 12 | onCursorMove(1.0)} 16 | onMouseMove={event => { 17 | const svg = event.currentTarget 18 | const rect = svg.getBoundingClientRect() 19 | const delta = (event.clientX - rect.left) / rect.width 20 | 21 | onCursorMove(delta) 22 | }} 23 | width={width} 24 | height={height} 25 | > 26 | {barsCount && 27 | Array(barsCount) 28 | .fill() 29 | .map((_, index) => { 30 | const y = index / barsCount 31 | 32 | return ( 33 | 43 | ) 44 | })} 45 | 46 | {points.map((point, idx) => { 47 | if (!idx) return false 48 | 49 | const prevPoint = points[idx - 1] 50 | const t = point[0] 51 | return ( 52 | 62 | ) 63 | })} 64 | 65 | {points.map(point => { 66 | let [x, y, throttleFactor] = point 67 | throttleFactor = throttleFactor || 0.0 68 | 69 | return ( 70 | 78 | ) 79 | })} 80 | 81 | ) 82 | } 83 | 84 | export default Timeline 85 | -------------------------------------------------------------------------------- /src/slides/transistor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Motion, spring, presets } from 'react-motion' 3 | 4 | import { Slide } from 'presa' 5 | import DynamicCode from 'blocks/dynamic-code' 6 | import Button from 'blocks/button' 7 | 8 | import './transistor.scss' 9 | 10 | const cycleRot = (collection, el) => 11 | collection[(collection.indexOf(el) + 1) % collection.length] 12 | 13 | const TransistorCode = ({ points }) => ( 14 | 15 | {'
    \n'} 16 | {'
    \n'} 21 | {' ...\n'} 22 | {'
    '} 23 | 24 | ) 25 | 26 | const TransistorCodeMotion = ({ points }) => { 27 | const p = points[0] 28 | 29 | return ( 30 | 31 | {'
    \n'} 32 | 33 | {' 35 | {'), y: spring('} 36 | 37 | {')}}>\n'} 38 | 39 | 46 | {v => ( 47 | 48 | {`
    \n`} 53 | 54 | )} 55 | 56 | {' \n'} 57 | {' ...\n'} 58 | {'
    '} 59 | 60 | ) 61 | } 62 | 63 | const TransistorPoints = ({ points, withMotion, width, height }) => { 64 | const pointsList = points.map((p, i) => { 65 | if (withMotion) { 66 | return ( 67 | 75 | {value => ( 76 |
    81 | )} 82 | 83 | ) 84 | } 85 | 86 | const pointTransform = `translate(${p.x}px, ${p.y}px)` 87 | return ( 88 |
    96 | ) 97 | }) 98 | 99 | return ( 100 |
    101 | {pointsList} 102 |
    103 | ) 104 | } 105 | 106 | class Transistor extends Component { 107 | constructor() { 108 | super() 109 | this.state = { 110 | mode: 'line', 111 | motionEnabled: true 112 | } 113 | } 114 | 115 | changeMode() { 116 | const modes = ['line', 'circle', 'eight'] 117 | this.setState({ mode: cycleRot(modes, this.state.mode) }) 118 | } 119 | 120 | toggleMotion() { 121 | this.setState({ motionEnabled: !this.state.motionEnabled }) 122 | } 123 | 124 | render() { 125 | const mode = this.state.mode 126 | const width = 700 127 | const height = 330 128 | 129 | const N = this.props.numberOfPoints 130 | 131 | const radius = 120.0 132 | const step = 22.0 133 | 134 | const motionEnabled = this.props.motionEnabled 135 | ? this.state.motionEnabled 136 | : false 137 | 138 | const points = Array(N) 139 | .fill() 140 | .map((_, idx) => { 141 | const t = (idx + 1) * Math.PI * 2 / N 142 | 143 | switch (mode) { 144 | case 'line': 145 | return { 146 | x: width * 0.5 + idx * step - N * step * 0.5, 147 | y: height * 0.5 148 | } 149 | 150 | case 'circle': 151 | return { 152 | x: width * 0.5 + radius * Math.cos(t), 153 | y: height * 0.5 + radius * Math.sin(t) 154 | } 155 | 156 | case 'eight': 157 | return { 158 | x: 159 | width * 0.5 + 160 | radius * 161 | Math.sqrt(2) * 162 | Math.cos(t) / 163 | (Math.pow(Math.sin(t), 2) + 1), 164 | y: 165 | height * 0.5 + 166 | radius * 167 | Math.sqrt(2) * 168 | Math.cos(t) * 169 | Math.sin(t) / 170 | (Math.pow(Math.sin(t), 2) + 1) 171 | } 172 | } 173 | }) 174 | 175 | const translations = { 176 | line: 'Line', 177 | circle: 'Circle', 178 | eight: 'Infinity' 179 | } 180 | 181 | return ( 182 | 183 |
    184 | {motionEnabled ? ( 185 | 186 | ) : ( 187 | 188 | )} 189 |
    190 | 191 | 197 | 198 |
    199 | {this.props.motionEnabled && ( 200 | 207 | )} 208 | 209 | 213 |
    214 |
    215 | ) 216 | } 217 | } 218 | 219 | Transistor.defaultProps = { 220 | numberOfPoints: 28 221 | } 222 | 223 | export default Transistor 224 | -------------------------------------------------------------------------------- /src/slides/transistor.scss: -------------------------------------------------------------------------------- 1 | 2 | .transistor-slide { 3 | display: flex; 4 | flex-flow: column nowrap; 5 | align-items: center; 6 | } 7 | 8 | .transistor-slide__code { 9 | background-color: rgba(yellow, 0.1); 10 | align-self: stretch; 11 | box-sizing: border-box; 12 | padding: 20px; 13 | } 14 | 15 | .transistor-slide__controls { 16 | .button { 17 | margin-right: 6px; 18 | } 19 | } 20 | 21 | .transistor__area { 22 | position: relative; 23 | } 24 | 25 | .transistor__point { 26 | width: 16px; 27 | height: 16px; 28 | left: -8px; 29 | top: -8px; 30 | 31 | background-color: black; 32 | border-radius: 26px; 33 | display: inline-block; 34 | position: absolute; 35 | 36 | transition: all 0.25s ease; 37 | } 38 | 39 | .transistor__point--no-transition { 40 | transition: none; 41 | } 42 | -------------------------------------------------------------------------------- /src/slides/transitions/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { Slide } from 'presa' 4 | import { Code } from 'presa/blocks' 5 | 6 | import FigureCaption from 'blocks/figure-caption' 7 | 8 | export class ReactMotionCodeSlide extends Component { 9 | render() { 10 | return ( 11 | 12 | {` 13 | 14 | {interpolated => 15 |
    } 16 | `} 17 | 18 | React Motion uses
    19 | function-as-a-prop pattern. 20 |
    21 | 22 | ) 23 | } 24 | } 25 | 26 | export class CssTransitionCodeSlide extends Component { 27 | render() { 28 | return ( 29 | 30 | {` 31 | // CSS property 32 | // transition: transform 1s ease; 33 | 34 | // Conditional state change 35 |
    36 | 37 | // Direct style manipulation 38 |
    `} 39 | 40 | CSS transitions work out of box in React.
    41 | transition property + state change → animation. 42 |
    43 | 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 4 | 5 | const isProduction = process.env.NODE_ENV === 'production' 6 | 7 | module.exports = { 8 | entry: './src/application.js', 9 | 10 | output: { 11 | path: path.resolve(__dirname, 'public'), 12 | filename: 'application.js' 13 | }, 14 | 15 | resolve: { 16 | symlinks: false, 17 | modules: [path.resolve(__dirname, 'src'), 'node_modules'] 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.jsx?$/, 24 | exclude: /node_modules/, 25 | use: ['babel-loader'] 26 | }, 27 | { 28 | test: /\.scss$/, 29 | use: ['style-loader', 'css-loader', 'sass-loader'] 30 | } 31 | ] 32 | }, 33 | 34 | plugins: isProduction 35 | ? [ 36 | new webpack.DefinePlugin({ 37 | 'process.env': { 38 | NODE_ENV: JSON.stringify('production') 39 | } 40 | }), 41 | new UglifyJsPlugin() 42 | ] 43 | : [], 44 | 45 | devtool: isProduction ? false : 'cheap-module-eval-source-map', 46 | 47 | devServer: { 48 | contentBase: path.join(__dirname, 'public') 49 | } 50 | } 51 | --------------------------------------------------------------------------------