├── static ├── music │ ├── mozart.mp3 │ ├── mozart.ogg │ ├── waltz.m4a │ ├── waltz.mp3 │ └── waltz.ogg ├── sounds │ ├── snare.mp3 │ ├── snare.ogg │ ├── snare.wav │ ├── hihat3.mp3 │ ├── hihat3.ogg │ ├── hihat3.wav │ ├── silence.mp3 │ ├── silence.ogg │ ├── silence.wav │ ├── snare2.wav │ ├── bassdrum4.mp3 │ ├── bassdrum4.ogg │ └── bassdrum4.wav └── images │ ├── cursor.png │ ├── cursor2.png │ ├── Cursor │ ├── Pointer.png │ └── Pointer.svg │ ├── audio.svg │ ├── quill.svg │ ├── vinyl.svg │ └── Pointer.svg ├── data └── example-data.json ├── components ├── default │ ├── graphic.js │ ├── inline.js │ ├── fixed.js │ ├── aside.js │ ├── preload.js │ ├── code-highlight.js │ ├── svg.js │ ├── step.js │ ├── text-container.js │ ├── float.js │ ├── button.js │ ├── link.js │ ├── boolean.js │ ├── text-input.js │ ├── action.js │ ├── stepper-control.js │ ├── analytics.js │ ├── range.js │ ├── display.js │ ├── select.js │ ├── radio.js │ ├── index.js │ ├── dynamic.js │ ├── table.js │ ├── header.js │ ├── chart.js │ ├── gist.js │ ├── stepper.js │ ├── equation.js │ └── scroller.js ├── Label.js ├── Clickable.js ├── Hoverable.js ├── Unmute.js ├── AudioPlayerSix.js ├── AudioPlayer.js ├── CircleGraphic.js ├── BeatCount.js ├── SixEightDemo.js ├── ThreeFourDemo.js └── LinearBeats.js ├── analytics ├── context.js └── index.js ├── README.md ├── package.json ├── .npmignore ├── .gitignore ├── styles.css └── index.idyll /static/music/mozart.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/music/mozart.mp3 -------------------------------------------------------------------------------- /static/music/mozart.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/music/mozart.ogg -------------------------------------------------------------------------------- /static/music/waltz.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/music/waltz.m4a -------------------------------------------------------------------------------- /static/music/waltz.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/music/waltz.mp3 -------------------------------------------------------------------------------- /static/music/waltz.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/music/waltz.ogg -------------------------------------------------------------------------------- /static/sounds/snare.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/snare.mp3 -------------------------------------------------------------------------------- /static/sounds/snare.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/snare.ogg -------------------------------------------------------------------------------- /static/sounds/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/snare.wav -------------------------------------------------------------------------------- /static/images/cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/images/cursor.png -------------------------------------------------------------------------------- /static/images/cursor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/images/cursor2.png -------------------------------------------------------------------------------- /static/sounds/hihat3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/hihat3.mp3 -------------------------------------------------------------------------------- /static/sounds/hihat3.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/hihat3.ogg -------------------------------------------------------------------------------- /static/sounds/hihat3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/hihat3.wav -------------------------------------------------------------------------------- /static/sounds/silence.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/silence.mp3 -------------------------------------------------------------------------------- /static/sounds/silence.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/silence.ogg -------------------------------------------------------------------------------- /static/sounds/silence.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/silence.wav -------------------------------------------------------------------------------- /static/sounds/snare2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/snare2.wav -------------------------------------------------------------------------------- /static/sounds/bassdrum4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/bassdrum4.mp3 -------------------------------------------------------------------------------- /static/sounds/bassdrum4.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/bassdrum4.ogg -------------------------------------------------------------------------------- /static/sounds/bassdrum4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/sounds/bassdrum4.wav -------------------------------------------------------------------------------- /static/images/Cursor/Pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megan-vo/basic-beats/HEAD/static/images/Cursor/Pointer.png -------------------------------------------------------------------------------- /data/example-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": 0, 4 | "y": 0 5 | }, { 6 | "x": 1, 7 | "y": 1 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /components/default/graphic.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Graphic extends React.Component { 4 | render() { 5 | const { idyll, updateProps, hasError, ...props } = this.props; 6 | return ( 7 |
8 | ); 9 | } 10 | } 11 | 12 | module.exports = Graphic; 13 | -------------------------------------------------------------------------------- /analytics/context.js: -------------------------------------------------------------------------------- 1 | const Analytics = require('./index'); 2 | 3 | module.exports = (context) => { 4 | const initialState = context.data(); 5 | const analytics = new Analytics('beat-basics'); 6 | analytics.onLoad(() => { 7 | analytics.updateState(initialState); 8 | context.onUpdate((newState) => { 9 | analytics.updateState(newState); 10 | }); 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /components/default/inline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Inline extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | Inline._idyll = { 14 | name: "Inline", 15 | tagType: "open" 16 | } 17 | 18 | 19 | export default Inline; 20 | -------------------------------------------------------------------------------- /components/default/fixed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Fixed extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | 14 | Fixed._idyll = { 15 | name: "Fixed", 16 | tagType: "open" 17 | } 18 | 19 | export default Fixed; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # basic-beats 2 | 3 | A short interactive exploration introducing the differences between 3/4 and 6/8 time signatures for desktop. 4 | 5 | Written using [Tone.js](https://github.com/Tonejs/Tone.js) and [Idyll](https://github.com/idyll-lang/idyll). 6 | 7 | [Link to article.](https://megan-vo.github.io/basic-beats/) 8 | 9 | Migrated from this ol' [repo](https://github.com/megan-vo/threefour-sixeight). 10 | -------------------------------------------------------------------------------- /components/default/aside.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Aside extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |
8 | {this.props.children} 9 |
10 |
11 | ); 12 | } 13 | } 14 | 15 | 16 | Aside._idyll = { 17 | name: "Aside", 18 | tagType: "open" 19 | } 20 | 21 | export default Aside; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threeFour-sixEight", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "d3": "^4.0.0", 7 | "firebase": "^5.4.0", 8 | "idyll": "^3.8.0", 9 | "idyll-d3-component": "^2.0.0", 10 | "react-latex": "^1.2.0", 11 | "tone": "^0.12.80" 12 | }, 13 | "devDependencies": { 14 | "gh-pages": "^0.12.0" 15 | }, 16 | "idyll": { 17 | "layout": "blog", 18 | "context": "./analytics/context" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/default/preload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const imageCache = []; 4 | 5 | class Preloader extends React.PureComponent { 6 | componentDidMount() { 7 | const { images } = this.props; 8 | images.forEach((i) => { 9 | const img = new Image(); 10 | img.src = i; 11 | imageCache.push(img); 12 | }); 13 | } 14 | render () { 15 | return null; 16 | } 17 | } 18 | 19 | Preloader.defaultProps = { 20 | images: [] 21 | }; 22 | 23 | export default Preloader; 24 | -------------------------------------------------------------------------------- /components/default/code-highlight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SyntaxHighlighter from "react-syntax-highlighter/dist/light"; 3 | import style from 'react-syntax-highlighter/dist/styles/github'; 4 | 5 | class CodeHighlight extends React.PureComponent { 6 | render() { 7 | return {this.props.children.length ? this.props.children[0] : ''}; 8 | } 9 | } 10 | 11 | CodeHighlight.defaultProps = { 12 | children: [] 13 | } 14 | 15 | export default CodeHighlight; 16 | -------------------------------------------------------------------------------- /components/default/svg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InlineSVG from 'react-inlinesvg'; 3 | 4 | class SVG extends React.PureComponent { 5 | render() { 6 | return ( 7 | 8 | ); 9 | } 10 | } 11 | 12 | SVG.defaultProps = { 13 | src: '' 14 | } 15 | 16 | SVG._idyll = { 17 | name: "SVG", 18 | tagType: "closed", 19 | props: [{ 20 | name: "src", 21 | type: "string", 22 | example: '"https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg"' 23 | }] 24 | } 25 | 26 | export default SVG; 27 | 28 | -------------------------------------------------------------------------------- /components/default/step.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let idx = 0; 4 | class Step extends React.Component { 5 | 6 | componentDidMount() { 7 | this.props.registerStep && this.props.registerStep(idx++, this.props.state, (this.props.onEnter || (() => {})).bind(this)); 8 | } 9 | render() { 10 | const { idyll, updateProps, hasError, registerStep, onEnter, state, className, ...props } = this.props; 11 | return ( 12 |
this.ref = ref} className={`idyll-step ${className || ''}`} {...props} /> 13 | ); 14 | } 15 | } 16 | 17 | export default Step; -------------------------------------------------------------------------------- /components/default/text-container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class TextContainer extends React.PureComponent { 4 | render() { 5 | const { idyll, children, className, hasError, updateProps, ...props } = this.props; 6 | const { styles, ...layout } = idyll.layout; 7 | const { styles: _, ...theme } = idyll.theme; 8 | const style = { ...layout, ...theme }; 9 | const cn = (className || '') + ' idyll-text-container'; 10 | return ( 11 |
{children}
12 | ); 13 | } 14 | } 15 | 16 | export default TextContainer; 17 | -------------------------------------------------------------------------------- /components/Label.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Label extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = ({ 7 | text: props.text 8 | }); 9 | } 10 | 11 | componentDidMount() { 12 | 13 | } 14 | 15 | 16 | 17 | // Toggles play on and off and creates a synth 18 | 19 | 20 | render() { 21 | const { hasError, idyll, updateProps, ...props } = this.props; 22 | return ( 23 |
24 |

{this.state.text}

25 |
26 | ) 27 | } 28 | } 29 | 30 | module.exports = Label; -------------------------------------------------------------------------------- /components/default/float.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Float extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | 14 | Float._idyll = { 15 | name: "Float", 16 | tagType: "open", 17 | props: [{ 18 | name: "position", 19 | type: "string", 20 | example: '"left"' 21 | }, { 22 | name: 'width', 23 | type: 'string', 24 | example: '"50%"' 25 | }] 26 | } 27 | 28 | export default Float; 29 | -------------------------------------------------------------------------------- /components/Clickable.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | // TODO Hover text effect 4 | 5 | // Works just like incrementer on the docs 6 | class Clickable extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {word: this.props.word}; 10 | } 11 | 12 | increment() { 13 | this.props.updateProps({ 14 | value: !this.props.value 15 | }) 16 | } 17 | 18 | render() { 19 | return( 20 |
21 | {this.state.word} 22 |
23 | ) 24 | } 25 | } 26 | 27 | module.exports = Clickable; -------------------------------------------------------------------------------- /components/default/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Button extends React.PureComponent { 4 | render() { 5 | const { onClick, idyll, hasError, updateProps, ...props } = this.props; 6 | return ( 7 | 10 | ); 11 | } 12 | } 13 | 14 | Button.defaultProps = { 15 | onClick: function() {} 16 | }; 17 | 18 | Button._idyll = { 19 | name: "Button", 20 | tagType: "open", 21 | children: ['Click Me.'], 22 | props: [{ 23 | name: "onClick", 24 | type: "event", 25 | example: "`x += 1`" 26 | }] 27 | } 28 | export default Button; 29 | -------------------------------------------------------------------------------- /components/default/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Link extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | let props = {...this.props}; 10 | if (props.url) { 11 | props.href = props.url; 12 | } 13 | return ( 14 | 15 | {this.props.text || this.props.children} 16 | 17 | ); 18 | } 19 | } 20 | 21 | Link._idyll = { 22 | name: "Link", 23 | tagType: "closed", 24 | props: [{ 25 | name: "text", 26 | type: "string", 27 | example: '"Link Text"' 28 | }, { 29 | name: 'url', 30 | type: 'string', 31 | example: '"https://some.url/"' 32 | }] 33 | } 34 | 35 | export default Link; 36 | -------------------------------------------------------------------------------- /components/default/boolean.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Boolean extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | toggleCheckbox() { 9 | this.props.updateProps({ 10 | value: !this.props.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Boolean.defaultProps = { 23 | value: false 24 | }; 25 | 26 | 27 | Boolean._idyll = { 28 | name: "Boolean", 29 | tagType: "closed", 30 | props: [{ 31 | name: "value", 32 | type: "boolean", 33 | example: "x" 34 | }] 35 | } 36 | 37 | export default Boolean; 38 | -------------------------------------------------------------------------------- /components/default/text-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class TextInput extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | const { idyll, hasError, updateProps, ...props } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | TextInput._idyll = { 23 | name: "TextInput", 24 | tagType: "closed", 25 | props: [{ 26 | name: "value", 27 | type: "string", 28 | example: '"Hello"' 29 | }] 30 | } 31 | 32 | export default TextInput; 33 | -------------------------------------------------------------------------------- /components/default/action.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Action extends React.PureComponent { 4 | render() { 5 | const { onClick, idyll, hasError, updateProps, ...props } = this.props; 6 | 7 | return ( 8 | {this.props.children} 9 | ); 10 | } 11 | } 12 | 13 | Action._idyll = { 14 | name: "Action", 15 | tagType: "open", 16 | children: [ 17 | "action text" 18 | ], 19 | props: [{ 20 | name: "onClick", 21 | type: 'event', 22 | example: '`x = !x`' 23 | }, { 24 | name: "onMouseEnter", 25 | type: 'event', 26 | example: '`x = true`' 27 | }, { 28 | name: "onMouseLeave", 29 | type: 'event', 30 | example: '`x = false`' 31 | }] 32 | } 33 | 34 | export default Action; 35 | -------------------------------------------------------------------------------- /components/default/stepper-control.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class StepperControl extends React.Component { 4 | 5 | componentDidMount() { 6 | } 7 | render() { 8 | const { idyll, ...props } = this.props; 9 | return
10 |
11 | ← 12 |
13 |
14 | → 15 |
16 |
; 17 | 18 | 19 | // ( 20 | //
this.ref = ref} className={`idyll-step ${className || ''}`} style={{margin: '10vh 0 60vh 0'}} {...props} /> 21 | // ); 22 | } 23 | } 24 | 25 | export default StepperControl; -------------------------------------------------------------------------------- /components/Hoverable.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | // TODO MAYBE GRAY IT OUT IF ONE IS PLAYING 4 | 5 | class Hoverable extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {word: this.props.word, 9 | weight: "normal"}; 10 | } 11 | 12 | display() { 13 | this.props.updateProps({ 14 | display: true, 15 | hover: true 16 | }); 17 | this.setState({weight: "bold"}); 18 | } 19 | 20 | reset() { 21 | this.props.updateProps({ 22 | display: false, 23 | hover: false 24 | }); 25 | this.setState({weight: "normal"}); 26 | } 27 | 28 | render() { 29 | return( 30 | 31 | {this.state.word} 32 | 33 | ) 34 | } 35 | } 36 | 37 | module.exports = Hoverable; -------------------------------------------------------------------------------- /components/default/analytics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Analytics extends React.PureComponent { 4 | componentDidMount() { 5 | try { 6 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 7 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 8 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 9 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 10 | 11 | ga('create', this.props.google, 'auto'); 12 | 13 | window.ga('send', 'pageview', { 14 | tag: this.props.tag 15 | }); 16 | } catch(e) { console.log('Could not mount Analytics.'); } 17 | } 18 | 19 | render() { 20 | return null; 21 | } 22 | } 23 | 24 | 25 | Analytics._idyll = { 26 | name: "Analytics", 27 | tagType: "closed", 28 | props: [{ 29 | name: "google", 30 | type: 'string', 31 | example: '"UA-XXXXXXX"' 32 | }] 33 | } 34 | 35 | 36 | 37 | export default Analytics; 38 | -------------------------------------------------------------------------------- /components/default/range.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Range extends React.PureComponent { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | handleChange(event) { 9 | this.props.updateProps({ 10 | value: +event.target.value 11 | }); 12 | } 13 | 14 | render() { 15 | const { value, min, max, step } = this.props; 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | Range.defaultProps = { 23 | value: 0, 24 | min: 0, 25 | max: 1, 26 | step: 1 27 | }; 28 | 29 | Range._idyll = { 30 | name: "Range", 31 | tagType: "closed", 32 | props: [{ 33 | name: "value", 34 | type: "number", 35 | example: "x" 36 | }, { 37 | name: "min", 38 | type: "number", 39 | example: '0' 40 | }, { 41 | name: "max", 42 | type: "number", 43 | example: '100' 44 | }, { 45 | name: "step", 46 | type: "number", 47 | example: '1' 48 | }] 49 | } 50 | 51 | export default Range; 52 | -------------------------------------------------------------------------------- /components/default/display.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const Format = require('d3-format'); 3 | 4 | class Display extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.format = Format.format(props.format || '0.2f'); 8 | } 9 | 10 | formatValue(v) { 11 | const t = typeof v; 12 | switch(t) { 13 | case 'object': 14 | return JSON.stringify(v); 15 | case 'number': 16 | return this.format(v); 17 | case 'string': 18 | default: 19 | return v; 20 | } 21 | } 22 | 23 | render() { 24 | const { value } = this.props; 25 | const v = value !== undefined ? value : this.props.var; 26 | return ( 27 | 28 | {this.formatValue(v)} 29 | 30 | ); 31 | } 32 | } 33 | 34 | 35 | Display._idyll = { 36 | name: "Display", 37 | tagType: "closed", 38 | props: [{ 39 | name: "value", 40 | type: "number", 41 | example: "x" 42 | }, { 43 | name: "format", 44 | type: "string", 45 | example: '"0.2f"' 46 | }] 47 | } 48 | 49 | export default Display; 50 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idyll 61 | build 62 | -------------------------------------------------------------------------------- /components/default/select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | 4 | class Select extends React.PureComponent { 5 | constructor(props) { 6 | super(props); 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.updateProps({ value: e.target.value }); 12 | } 13 | 14 | render() { 15 | const { idyll, hasError, updateProps, ...props } = this.props; 16 | return ( 17 | 25 | ); 26 | } 27 | } 28 | 29 | Select.defaultProps = { 30 | options: [] 31 | } 32 | 33 | Select._idyll = { 34 | name: "Select", 35 | tagType: "closed", 36 | props: [{ 37 | name: "value", 38 | type: "string", 39 | example: "x" 40 | }, { 41 | name: "options", 42 | type: "array", 43 | example: '`["option1", "option2"]`' 44 | }] 45 | } 46 | export default Select; 47 | -------------------------------------------------------------------------------- /components/default/radio.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | let id = 0; 4 | 5 | class Radio extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | this.onChange = this.onChange.bind(this); 9 | this.id = id++; 10 | } 11 | 12 | onChange(e) { 13 | this.props.updateProps({ value: e.target.value }); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | {this.props.options.map((d) => { 20 | if (typeof d === 'string') { 21 | return ; 22 | } 23 | return ; 24 | })} 25 |
26 | ); 27 | } 28 | } 29 | 30 | Radio.defaultProps = { 31 | options: [] 32 | }; 33 | 34 | 35 | Radio._idyll = { 36 | name: "Radio", 37 | tagType: "closed", 38 | props: [{ 39 | name: "value", 40 | type: "string", 41 | example: "x" 42 | }, { 43 | name: "options", 44 | type: "array", 45 | example: '`["option1", "option2"]`' 46 | }] 47 | } 48 | 49 | export default Radio; 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .idyll 64 | build/** 65 | 66 | .ipynb_checkpoints 67 | static/images/met/ 68 | static/images/thumbnails/met/ 69 | met-images 70 | 71 | analytics -------------------------------------------------------------------------------- /components/default/index.js: -------------------------------------------------------------------------------- 1 | export { default as Action } from './action'; 2 | export { default as Analytics } from './analytics'; 3 | export { default as Aside } from './aside'; 4 | export { default as Boolean } from './boolean'; 5 | export { default as Button } from './button'; 6 | export { default as Chart } from './chart'; 7 | export { default as CodeHighlight } from './code-highlight'; 8 | export { default as Display } from './display'; 9 | export { default as Dynamic } from './dynamic'; 10 | export { default as Equation } from './equation'; 11 | export { default as Fixed } from './fixed'; 12 | export { default as Float } from './float'; 13 | export { default as Gist } from './gist'; 14 | export { default as Header } from './header'; 15 | export { default as Inline } from './inline'; 16 | export { default as Link } from './link'; 17 | export { default as Preload } from './preload'; 18 | export { default as Radio } from './radio'; 19 | export { default as Range } from './range'; 20 | export { default as Select } from './select'; 21 | export { default as Step } from './step'; 22 | export { default as Stepper } from './stepper'; 23 | export { default as StepperControl } from './stepper-control'; 24 | export { default as SVG } from './svg'; 25 | export { default as Table } from './table'; 26 | export { default as TextContainer } from './text-container'; 27 | export { default as TextInput } from './text-input'; 28 | 29 | -------------------------------------------------------------------------------- /components/Unmute.js: -------------------------------------------------------------------------------- 1 | // Audio play button that user clicks on to allow audio to play on page 2 | // to satisfy audio policy 3 | 4 | const React = require("react"); 5 | const StartAudioContext = require("startaudiocontext"); 6 | 7 | var Tone; 8 | 9 | class Unmute extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | on: false 14 | }; 15 | 16 | this.buttonClick = this.buttonClick.bind(this); 17 | } 18 | 19 | componentDidMount() { 20 | Tone = require("tone"); 21 | StartAudioContext(Tone.context, "#unmute"); 22 | 23 | document.querySelector("body").addEventListener("click", e => { 24 | this.buttonClick(); 25 | }); 26 | } 27 | 28 | buttonClick() { 29 | if (!this.state.on) { 30 | this.setState({ 31 | on: !this.state.on 32 | }); 33 | } 34 | } 35 | 36 | render() { 37 | return [ 38 |
39 | 59 |
60 | ]; 61 | } 62 | } 63 | 64 | module.exports = Unmute; 65 | -------------------------------------------------------------------------------- /components/default/dynamic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Format = require('d3-format'); 4 | const Drag = require('d3-drag'); 5 | const Selection = require('d3-selection'); 6 | 7 | class Dynamic extends React.PureComponent { 8 | 9 | componentDidMount() { 10 | let node; 11 | try { 12 | node = ReactDOM.findDOMNode(this); 13 | } catch(e) {}; 14 | if (!node) { 15 | return; 16 | } 17 | this.drag = Drag.drag().on('drag', () => { 18 | const dx = Selection.event.dx; 19 | const { step, value, interval } = this.props; 20 | const newValue = Math.max(Math.min(value + (step || interval) * dx, this.props.max), this.props.min); 21 | this.props.updateProps({ value: newValue }); 22 | }); 23 | this.drag(Selection.select(node)); 24 | } 25 | 26 | render() { 27 | const { format, value } = this.props; 28 | const formatter = Format.format(format); 29 | return ( 30 | 31 | {formatter(value)} 32 | 33 | ); 34 | } 35 | } 36 | 37 | Dynamic.defaultProps = { 38 | format: '.2f', 39 | min: Number.NEGATIVE_INFINITY, 40 | max: Number.POSITIVE_INFINITY, 41 | step: 1 42 | }; 43 | 44 | 45 | Dynamic._idyll = { 46 | name: "Dynamic", 47 | tagType: "closed", 48 | props: [{ 49 | name: "value", 50 | type: "number", 51 | example: "x" 52 | }, { 53 | name: "step", 54 | type: "string", 55 | example: '1' 56 | }, { 57 | name: "min", 58 | type: "number", 59 | example: '-100' 60 | }, { 61 | name: "max", 62 | type: "number", 63 | example: '100' 64 | }] 65 | } 66 | 67 | export default Dynamic; 68 | -------------------------------------------------------------------------------- /components/AudioPlayerSix.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | var Tone; 4 | var player; 5 | 6 | class AudioPlayerSix extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | file: props.file 11 | } 12 | } 13 | 14 | componentDidMount() { 15 | Tone = require('tone'); 16 | player = new Tone.Player("static/music/" + this.state.file).toMaster(); 17 | this.setState({ mounted: true }); 18 | } 19 | 20 | 21 | 22 | // Toggles play on and off and creates a synth 23 | // to be played. Changes the button text to 24 | // on/off 25 | playAudio() { 26 | // Play the audio when loaded and clicked and the transport isn't playing anything 27 | if (this.state.mounted && !this.state.play && Tone.Transport.state === "stopped") { 28 | this.props.updateProps({ 29 | sync: true 30 | }) 31 | player.start(); 32 | Tone.Transport.start(); 33 | } else { 34 | this.turnOff(); 35 | } 36 | document.getElementById("audioSixPtr").classList.add("hide"); 37 | } 38 | 39 | turnOff() { 40 | this.props.updateProps({ 41 | sync: false 42 | }) 43 | Tone.Transport.stop(); 44 | player.stop(); 45 | document.getElementById("audioSixPtr").classList.remove("hide"); 46 | } 47 | 48 | render() { 49 | const { hasError, idyll, updateProps, ...props } = this.props; 50 | return ( 51 |
52 |

6/8

53 | 54 | 55 |
56 | ) 57 | } 58 | } 59 | 60 | module.exports = AudioPlayerSix; -------------------------------------------------------------------------------- /components/AudioPlayer.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | var Tone; 4 | var player; 5 | 6 | class AudioPlayer extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | file: props.file 11 | } 12 | } 13 | 14 | componentDidMount() { 15 | Tone = require('tone'); 16 | player = new Tone.Player("static/music/" + this.state.file).toMaster(); 17 | this.setState({ mounted: true }); 18 | } 19 | 20 | 21 | 22 | // Toggles play on and off and creates a synth 23 | // to be played. Changes the button text to 24 | // on/off 25 | playAudio() { 26 | // Play the audio when loaded and clicked and the transport isn't playing anything 27 | if (this.state.mounted && !this.state.play && Tone.Transport.state === "stopped") { 28 | this.props.updateProps({ 29 | sync: true 30 | }) 31 | player.start(); 32 | Tone.Transport.start(); 33 | } else { 34 | this.turnOff(); 35 | } 36 | document.getElementById("audioThreePtr").classList.add("hide"); 37 | } 38 | 39 | turnOff() { 40 | this.props.updateProps({ 41 | sync: false 42 | }) 43 | Tone.Transport.stop(); 44 | player.stop(); 45 | document.getElementById("audioThreePtr").classList.remove("hide"); 46 | 47 | } 48 | 49 | render() { 50 | const { hasError, idyll, updateProps, ...props } = this.props; 51 | return ( 52 |
53 |

3/4

54 | 55 | 56 |
57 | ) 58 | } 59 | } 60 | 61 | module.exports = AudioPlayer; -------------------------------------------------------------------------------- /components/default/table.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const Table = require('react-table').default; 3 | 4 | class TableComponent extends React.PureComponent { 5 | getColumns() { 6 | if (this.props.columns) { 7 | if (this.props.columns.length && typeof this.props.columns[0] === 'string') { 8 | return this.props.columns.map((d) => { 9 | return { 10 | Header: d, 11 | accessor: d 12 | }; 13 | }) 14 | } 15 | 16 | return this.props.columns; 17 | } 18 | if ((this.props.data || []).length) { 19 | return Object.keys(this.props.data[0]).map((d) => { 20 | return { 21 | Header: d, 22 | accessor: d 23 | } 24 | }) 25 | } 26 | 27 | return []; 28 | } 29 | render() { 30 | return ( 31 | this.props.defaultPageSize} 34 | minRows={this.props.data.length <= this.props.defaultPageSize ? this.props.data.length : undefined} 35 | {...this.props} 36 | children={undefined} 37 | columns={this.getColumns()} 38 | /> 39 | ); 40 | } 41 | } 42 | 43 | TableComponent.defaultProps = { 44 | data: [], 45 | showPageSizeOptions: false, 46 | showPageJump: false, 47 | defaultPageSize: 20 48 | } 49 | 50 | TableComponent._idyll = { 51 | name: "Table", 52 | tagType: "closed", 53 | props: [{ 54 | name: "data", 55 | type: "array", 56 | example: 'x' 57 | }, { 58 | name: "showPagination", 59 | type: "boolean", 60 | example: 'false' 61 | }, { 62 | name: "showPageSizeOptions", 63 | type: "boolean", 64 | example: 'false' 65 | }, { 66 | name: "showPageJump", 67 | type: "boolean", 68 | example: 'false' 69 | }] 70 | } 71 | 72 | export default TableComponent; 73 | -------------------------------------------------------------------------------- /static/images/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | high-volume 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /components/default/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Header extends React.PureComponent { 4 | render() { 5 | return ( 6 |
7 |

8 | {this.props.title} 9 |

10 | { 11 | this.props.subtitle && ( 12 |

13 | {this.props.subtitle} 14 |

15 | ) 16 | } 17 | { 18 | this.props.author && ( 19 |
20 | By: {this.props.author} 21 |
22 | ) 23 | } 24 | { 25 | this.props.authors ? ( 26 |
27 | By: { 28 | this.props.authors.map((author, i) => { 29 | if (typeof author === 'string') { 30 | return author; 31 | } 32 | return author.link ? ( 33 | 34 | {author.name}{ 35 | i < this.props.authors.length -1 ? ( 36 | i === this.props.authors.length - 2 ? ' and ' : ', ' ) 37 | : ''} 38 | 39 | ) : author.name; 40 | }) 41 | } 42 | {} 43 |
44 | ) : null 45 | } 46 | { 47 | this.props.date && ( 48 |
49 | {this.props.date} 50 |
51 | ) 52 | } 53 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | Header._idyll = { 60 | name: "Header", 61 | tagType: "closed", 62 | props: [{ 63 | name: "title", 64 | type: "string", 65 | example: '"Article Title"' 66 | }, { 67 | name: 'subtitle', 68 | type: 'string', 69 | example: '"Article subtitle."' 70 | }, { 71 | name: 'author', 72 | type: 'string', 73 | example: '"Author Name"' 74 | }, { 75 | name: 'authorLink', 76 | type: 'string', 77 | example: '"author.website"' 78 | }] 79 | } 80 | 81 | export default Header; 82 | -------------------------------------------------------------------------------- /static/images/quill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /components/CircleGraphic.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | const centerX = 200; 4 | const centerY = 150; 5 | const radius = 100; 6 | 7 | class CircleGraphic extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | numCircles: props.numCircles, 12 | placement: props.placement, 13 | circleTags: [], 14 | textTags: [], 15 | }; 16 | 17 | // Set up the initial smaller circle states 18 | // And compute their positions based on props passed in 19 | for (var i = 0; i < this.state.numCircles; i++) { 20 | var newX = centerX + radius * Math.cos((this.state.placement[i] + 180) * Math.PI / 180); 21 | var newY = centerY + radius * Math.sin((this.state.placement[i] + 180) * Math.PI / 180); 22 | 23 | // The tags to push in 24 | var circles = ; 25 | var text = {(i + 1)}; 26 | 27 | this.setState({ circleTags: this.state.circleTags.push(circles) }); 28 | this.setState({ textTags: this.state.textTags.push(text) }); 29 | } 30 | } 31 | 32 | // Create tags around circles to make sure 33 | // their opacity is correct on the beats when rendered 34 | renderTags(tags) { 35 | var newResult = []; 36 | for (var i = 0; i < this.state.numCircles; i++) { 37 | newResult.push( 38 | {tags[i]}); 39 | } 40 | return newResult; 41 | } 42 | 43 | render() { 44 | const { opacity, rotation, showText, hasError, idyll, updateProps, ...props } = this.props; 45 | return ( 46 | 50 | {this.props.label} 51 | 52 | 53 | 54 | 55 | 56 | {this.renderTags(this.state.circleTags)} 57 | {showText ? this.renderTags(this.state.textTags) : () => { }} 58 | 59 | 60 | 61 | ) 62 | } 63 | } 64 | 65 | module.exports = CircleGraphic; -------------------------------------------------------------------------------- /components/default/chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const V = require('victory'); 3 | const d3Arr = require('d3-array'); 4 | 5 | const types = { 6 | AREA: V.VictoryArea, 7 | TIME: V.VictoryLine, 8 | LINE: V.VictoryLine, 9 | BAR: V.VictoryBar, 10 | SCATTER: V.VictoryScatter, 11 | PIE: V.VictoryPie 12 | }; 13 | 14 | let chartCount = 0; 15 | 16 | class Chart extends React.PureComponent { 17 | 18 | constructor(props) { 19 | super(props); 20 | this.id = chartCount++; 21 | } 22 | 23 | render() { 24 | const { id, props } = this; 25 | const type = props.type.toUpperCase(); 26 | const INNER_CHART = types[type]; 27 | let { 28 | scale, 29 | data, 30 | domain, 31 | range, 32 | domainPadding = 10, 33 | animate, 34 | ...customProps } = props; 35 | 36 | if (props.equation) { 37 | const d = domain; 38 | data = d3Arr.range(d[0], d[1], (d[1] - d[0]) / props.samplePoints).map((x) => { 39 | try { 40 | return { 41 | x: x, 42 | y: props.equation(x) 43 | }; 44 | } catch(err) { 45 | return { 46 | x: x, 47 | y: 0 48 | } 49 | } 50 | }); 51 | } 52 | 53 | if (type === types.TIME) { 54 | scale = {x: 'time', y: 'linear'}; 55 | data = data.map((d) => { 56 | return Object.assign({}, d, { 57 | x: new Date(d.x) 58 | }); 59 | }); 60 | } 61 | return ( 62 |
63 | {type !== 'PIE' ? ( 64 | 65 | 70 | 71 | 72 | ) : ( 73 | 74 | 75 | ) 76 | } 77 |
78 | ); 79 | } 80 | } 81 | 82 | Chart.defaultProps = { 83 | domain: [-1, 1], 84 | range: [-1, 1], 85 | domainPadding: 0, 86 | samplePoints: 100, 87 | type: 'line' 88 | }; 89 | 90 | 91 | Chart._idyll = { 92 | name: "Chart", 93 | tagType: "closed", 94 | props: [{ 95 | name: "type", 96 | type: "string", 97 | example: '"scatter"' 98 | },{ 99 | name: "data", 100 | type: "array", 101 | example: "`[{x: 1, y: 1}, { x: 2, y: 2 }]`" 102 | }] 103 | } 104 | 105 | export default Chart; 106 | -------------------------------------------------------------------------------- /components/default/gist.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const PropTypes = require('prop-types'); 3 | 4 | class EmbeddedGist extends React.PureComponent { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.gist = props.gist; 9 | this.file = props.file; 10 | this.stylesheetAdded = false; 11 | this.state = { 12 | loading: true, 13 | src: "" 14 | }; 15 | } 16 | 17 | // The Gist JSON data includes a stylesheet to add to the page 18 | // to make it look correct. `addStylesheet` ensures we only add 19 | // the stylesheet one time. 20 | addStylesheet(href) { 21 | if (!this.stylesheetAdded) { 22 | this.stylesheetAdded = true; 23 | var link = document.createElement('link'); 24 | link.type = "text/css"; 25 | link.rel = "stylesheet"; 26 | link.href = href; 27 | 28 | (document.head || document.body || {appendChild: () => {}}).appendChild(link); 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | // Create a JSONP callback that will set our state 34 | // with the data that comes back from the Gist site 35 | var gistCallback = EmbeddedGist.nextGistCallback(); 36 | window[gistCallback] = function(gist) { 37 | this.setState({ 38 | loading: false, 39 | src: gist.div 40 | }); 41 | this.addStylesheet(gist.stylesheet); 42 | }.bind(this); 43 | 44 | var url = "https://gist.github.com/" + this.props.gist + ".json?callback=" + gistCallback; 45 | if (this.props.file) { 46 | url += "&file=" + this.props.file; 47 | } 48 | 49 | // Add the JSONP script tag to the document. 50 | var script = document.createElement('script'); 51 | script.type = 'text/javascript'; 52 | script.src = url; 53 | (document.head || document.body || {appendChild: () => {}}).appendChild(script); 54 | } 55 | 56 | render() { 57 | if (this.state.loading) { 58 | return
loading...
; 59 | } else { 60 | return
; 61 | } 62 | } 63 | } 64 | 65 | EmbeddedGist.propTypes = { 66 | gist: PropTypes.string.isRequired, // e.g. "username/id" 67 | file: PropTypes.string // to embed a single specific file from the gist 68 | }; 69 | 70 | // Each time we request a Gist, we'll need to generate a new 71 | // global function name to serve as the JSONP callback. 72 | var gistCallbackId = 0; 73 | EmbeddedGist.nextGistCallback = () => { 74 | return "embed_gist_callback_" + gistCallbackId++; 75 | }; 76 | 77 | EmbeddedGist.defaultProps = { 78 | gist: 'mathisonian/689614257cb1af6b15de3344da6cdc7a' 79 | } 80 | 81 | EmbeddedGist._idyll = { 82 | name: "Gist", 83 | tagType: "closed", 84 | props: [{ 85 | name: "gist", 86 | type: "string", 87 | example: '"0f83a12e29b268ffca39f471ecf39e91"' 88 | }, { 89 | name: 'file', 90 | type: 'string', 91 | example: '"particles.idl"' 92 | }] 93 | } 94 | export default EmbeddedGist; 95 | 96 | -------------------------------------------------------------------------------- /components/default/stepper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const { filterChildren, mapChildren } = require('idyll-component-children'); 3 | 4 | class Stepper extends React.PureComponent { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.SCROLL_STEP_MAP = {}; 9 | this.SCROLL_NAME_MAP = {}; 10 | } 11 | 12 | 13 | registerStep(elt, name, val) { 14 | this.SCROLL_STEP_MAP[elt] = val; 15 | this.SCROLL_NAME_MAP[elt] = name; 16 | } 17 | 18 | getSteps() { 19 | return filterChildren( 20 | this.props.children || [], 21 | (c) => { 22 | return c.type.name && c.type.name.toLowerCase() === 'step'; 23 | } 24 | ) || [] 25 | } 26 | 27 | next() { 28 | this.props.updateProps({ currentStep: (this.props.currentStep + 1) % (this.getSteps().length) }); 29 | } 30 | previous() { 31 | let newStep = this.props.currentStep - 1; 32 | if (newStep < 0) { 33 | newStep = (this.getSteps().length) + newStep; 34 | } 35 | 36 | this.props.updateProps({ currentStep: newStep }); 37 | } 38 | 39 | getSelectedStep() { 40 | const { currentState, currentStep } = this.props; 41 | const steps = this.getSteps(); 42 | if (currentState) { 43 | return filterChildren( 44 | steps, 45 | (c) => { 46 | return c.props.state === currentState 47 | } 48 | )[0]; 49 | } 50 | return steps[currentStep % steps.length]; 51 | } 52 | 53 | render() { 54 | const { children, height, ...props } = this.props; 55 | return ( 56 |
57 |
58 | {filterChildren( 59 | children, 60 | (c) => { 61 | return c.type.name && c.type.name.toLowerCase() === 'graphic'; 62 | } 63 | )} 64 |
65 |
66 | { 67 | mapChildren(this.getSelectedStep(), (c) => { 68 | return React.cloneElement(c, { 69 | registerStep: this.registerStep.bind(this) 70 | }) 71 | }) 72 | } 73 |
74 | {mapChildren(filterChildren( 75 | children, 76 | (c) => { 77 | return c.type.name && c.type.name.toLowerCase() === 'steppercontrol'; 78 | } 79 | ), (c) => { 80 | return React.cloneElement(c, { 81 | next: this.next.bind(this), 82 | previous: this.previous.bind(this) 83 | }) 84 | })} 85 |
86 | ); 87 | } 88 | } 89 | 90 | 91 | Stepper.defaultProps = { 92 | currentStep: 0, 93 | height: 500 94 | }; 95 | 96 | Stepper._idyll = { 97 | name: "Stepper", 98 | tagType: "open", 99 | children: [` 100 | [Step]This is the content for step 1[/Step] 101 | [Step]This is the content for step 2[/Step] 102 | [Step]This is the content for step 3[/Step]`], 103 | props: [{ 104 | name: "currentStep", 105 | type: "number", 106 | example: '0' 107 | }] 108 | } 109 | export default Stepper; 110 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Autoplay handling style */ 2 | #unmuteContainer { 3 | display: flex; 4 | justify-content: center; 5 | margin-top: 5%; 6 | } 7 | 8 | #unmute { 9 | margin: 0 auto; 10 | margin-top: 5%; 11 | transform: scale(1.5); 12 | } 13 | 14 | .idyll-scroll { 15 | margin-top: 0; 16 | } 17 | 18 | .idyll-scroll-text { 19 | padding: 0 0; 20 | } 21 | 22 | .idyll-scroll-text .idyll-step { 23 | margin: 0; 24 | padding: 60px; 25 | background: white; 26 | font-family: "Noto Sans", sans-serif; 27 | } 28 | 29 | .article-header { 30 | width: 470px; 31 | max-width: 90vw; 32 | margin-bottom: 0; 33 | } 34 | 35 | .hoverable { 36 | cursor: pointer; 37 | } 38 | 39 | img { 40 | max-width: 100%; 41 | height: 20px; 42 | box-sizing: content-box; 43 | background-color: none; 44 | margin: auto auto 3% 50%; 45 | } 46 | 47 | .hide { 48 | visibility: hidden; 49 | opacity: 0; 50 | transition: visibility 0s 0.3s, opacity 0.3s linear; 51 | } 52 | 53 | .counts { 54 | height: 38px; 55 | } 56 | 57 | .beatCounts { 58 | background-color: #f9f9f9; 59 | padding: 0.8em; 60 | } 61 | 62 | button { 63 | margin: auto auto 6% 40%; 64 | color: #fff; 65 | cursor: pointer; 66 | outline: none; 67 | background: #edae49; 68 | border: none; 69 | height: 25px; 70 | } 71 | 72 | button:focus { 73 | outline: 0; 74 | } 75 | 76 | #audioThree { 77 | /* float: left; */ 78 | height: 5em; 79 | box-sizing: content-box; 80 | background-color: none; 81 | } 82 | 83 | #audioSixPtr { 84 | margin: 4% auto auto 60%; 85 | } 86 | 87 | #audioSix { 88 | /* float: left; */ 89 | height: 5em; 90 | box-sizing: content-box; 91 | background-color: none; 92 | /* margin: auto auto auto 25% */ 93 | } 94 | 95 | /* Idyll divs */ 96 | #audioSourceThree, 97 | #audioSourceSix { 98 | display: inline-block; 99 | } 100 | 101 | #audioSourceThree { 102 | margin: auto auto auto auto; 103 | } 104 | 105 | #label3 { 106 | position: absolute; 107 | margin: 1% 0 0 10%; 108 | color: #ff851b; 109 | } 110 | 111 | h1 { 112 | text-align: center; 113 | color: #edae49; 114 | font-family: "Anton", sans-serif; 115 | } 116 | 117 | h1.hed { 118 | font-size: 4em; 119 | } 120 | 121 | h2 { 122 | font-size: 1.3em; 123 | text-align: center; 124 | font-weight: 600; 125 | } 126 | 127 | .byline { 128 | text-align: center; 129 | } 130 | 131 | h3 { 132 | color: #087e8b; 133 | text-transform: uppercase; 134 | } 135 | 136 | .hoverableAudio { 137 | cursor: pointer; 138 | max-height: 100%; 139 | max-width: 50%; 140 | } 141 | 142 | h4 { 143 | color: #edae49; 144 | text-align: right; 145 | font-size: 1.2em; 146 | } 147 | 148 | @media (max-width: 560px) { 149 | button { 150 | margin: auto auto 10% 30%; 151 | } 152 | } 153 | 154 | @media (min-width: 512px) { 155 | #audioSourceSix { 156 | margin: auto auto auto 20%; 157 | } 158 | 159 | #audioSourceThree { 160 | margin-left: 10%; 161 | } 162 | } 163 | 164 | @media (max-width: 1000px) { 165 | .fixed { 166 | display: flex; 167 | flex-direction: row; 168 | } 169 | } 170 | 171 | @media (min-width: 1001px) { 172 | .fixed { 173 | margin-right: 10vw; 174 | } 175 | } 176 | 177 | @media (min-width: 1100px) { 178 | .fixed { 179 | margin-right: 1vw; 180 | } 181 | } 182 | 183 | @media only screen and (min-width: 2000px) { 184 | .idyll-text-container { 185 | max-width: 40vw !important; 186 | font-size: 22px; 187 | margin-left: 5%; 188 | } 189 | 190 | .fixed .hoverable { 191 | transform: scale(1.7); 192 | margin-bottom: 10vh; 193 | margin-top: 15vh; 194 | margin-right: 15vw; 195 | } 196 | 197 | .article-header { 198 | width: 100%; 199 | } 200 | 201 | button { 202 | margin: auto auto 6% 45%; 203 | } 204 | 205 | .ptrs { 206 | transform: scale(1.5); 207 | } 208 | 209 | .fixed div { 210 | width: 0%; 211 | } 212 | 213 | #audioSourceThree { 214 | margin-left: 20%; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /static/images/vinyl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | vinyl 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 | -------------------------------------------------------------------------------- /components/BeatCount.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class BeatCount extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | beatCounts() { 9 | var result = []; 10 | for (var i = 1; i <= this.props.upTo; i++) { 11 | var color = i === 1 ? "#FF851B" : "#087E8B"; 12 | var beatCount = this.props.beatCount; 13 | if (this.props.upTo === 3) { 14 | var fontWeightAnd = (i === 1 && beatCount === 2 || 15 | i === 2 && beatCount === 4 || 16 | i === 3 && beatCount === 6) ? "bold" : "normal"; 17 | result.push( 18 | {i + " "} 19 | and 20 | ); 21 | } else { 22 | var fontWeightAnd = (i === 1 && beatCount === 2 || i === 2 && beatCount === 5) ? "bold" : "normal"; 23 | var fontWeightAh = (i === 1 && beatCount === 3 || i === 2 && beatCount === 6) ? "bold" : "normal"; 24 | 25 | result.push( 26 | {i + " "} 27 | and 28 | ah 29 | ) 30 | } 31 | } 32 | return result; 33 | } 34 | 35 | hoverOn() { 36 | this.props.updateProps({ 37 | hover: true 38 | }) 39 | document.getElementById("ptr" + this.props.upTo).classList.add("hide"); 40 | } 41 | 42 | hoverOff() { 43 | this.props.updateProps({ 44 | hover: false 45 | }) 46 | document.getElementById("ptr" + this.props.upTo).classList.remove("hide"); 47 | } 48 | 49 | renderAlt() { 50 | // 1 2 3 4 5 6 51 | // 1 2 52 | var result = []; 53 | for (var i = 1; i <= this.props.upTo; i++) { 54 | var color = i === 1 ? "#FF851B" : "#087E8B"; 55 | var beatCount = this.props.beatCount; 56 | if (this.props.upTo === 3) { 57 | var fontWeightAnd = (i === 1 && beatCount === 2 || 58 | i === 2 && beatCount === 4 || 59 | i === 3 && beatCount === 6) ? "bold" : "normal"; 60 | result.push( 61 | {i * 2 - 1 + " "} 62 | {i * 2 + " "} 63 | ); 64 | } else { 65 | var fontWeightAnd = (i === 1 && beatCount === 2 || i === 2 && beatCount === 5) ? "bold" : "normal"; 66 | var fontWeightAh = (i === 1 && beatCount === 3 || i === 2 && beatCount === 6) ? "bold" : "normal"; 67 | 68 | result.push( 69 | {3 * i - 2 + " "} 70 | {3 * i - 1 + " "} 71 | {3 * i + " "} 72 | ) 73 | } 74 | } 75 | return result; 76 | } 77 | 78 | render() { 79 | const { altShow, hasError, idyll, updateProps, ...props } = this.props; 80 | 81 | return [ 82 |
83 |
84 |

{this.props.upTo !== 0 ? this.beatCounts() : () => { }}

85 |

{altShow ? this.renderAlt() : () => { }}

86 |
87 | 88 |
89 | ] 90 | } 91 | } 92 | 93 | module.exports = BeatCount; -------------------------------------------------------------------------------- /components/SixEightDemo.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import CircleGraphic from './CircleGraphic.js'; 3 | 4 | var Tone; 5 | var sampler; 6 | var pattern; 7 | 8 | class SixEightDemo extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | play: false, 13 | mounted: false, 14 | opacity: "0.8", 15 | onBeat: 0, 16 | rotation: "rotate(0 200 150)", 17 | degrees: 0 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | Tone = require('tone'); 23 | // creates it once to avoid overlapping synths 24 | sampler = new Tone.Sampler({ 25 | "C4": "static/sounds/bassdrum4.wav", 26 | "E4": "static/sounds/hihat3.wav", 27 | "D4": "static/sounds/snare.wav" 28 | }).toMaster(); 29 | 30 | // To avoid overlapping patterns, declare here 31 | // Allows stop and start to end where it left off 32 | pattern = new Tone.Sequence(function (time, note) { 33 | this.animateCircles(note, time); 34 | sampler.triggerAttackRelease(note, .25); 35 | }.bind(this), ["C4", "E4", "E4", "D4", "E4", "E4"], "4n"); 36 | 37 | // Make sure it is mounted before loading up 38 | // sampler 39 | this.setState({ mounted: true }); 40 | } 41 | 42 | // Animates the circle in sync with the current 43 | // note being played 44 | animateCircles(note, time) { 45 | Tone.Draw.schedule(function () { 46 | this.props.updateProps({ 47 | beatNum: ((this.props.beatNum) % 6) + 1 48 | }) 49 | 50 | this.setState({ onBeat: this.state.onBeat + 1 }); 51 | this.setState({ rotation: "rotate(" + this.state.degrees + " 200 150)" }); 52 | this.setState({ degrees: this.state.degrees + 60 }); 53 | 54 | }.bind(this), time); 55 | } 56 | 57 | // Toggles play on and off and creates a synth 58 | // to be played. Changes the button text to 59 | // on/off 60 | playAudio() { 61 | 62 | // Play the audio when loaded and clicked 63 | if (Tone.Transport.state === "stopped") { 64 | Tone.Transport.bpm.value = 120; 65 | this.turnOn("+0"); 66 | } else if (this.state.play) { 67 | this.turnOff(); 68 | } 69 | } 70 | 71 | turnOn(start) { 72 | if (this.state.mounted && !this.state.play) { 73 | this.setState({ degrees: 0 }); 74 | this.setState({ onBeat: 0 }); 75 | this.props.updateProps({ 76 | beatNum: 0 77 | }); 78 | 79 | // starts the transport and lets 80 | // us know that playback is on 81 | Tone.Transport.start(start); 82 | pattern.start(start); 83 | this.setState({ opacity: "1" }); 84 | this.setState({ play: true }); 85 | this.props.updateProps({ 86 | on: true, 87 | hover: true 88 | }); 89 | } else if (this.state.play) { 90 | this.turnOff(); 91 | } 92 | } 93 | 94 | turnOff() { 95 | // Stops transport and lets us know 96 | // playback is free to start playing 97 | // the next thing 98 | Tone.Transport.stop(); 99 | pattern.stop(); 100 | this.setState({ opacity: "0.6" }); 101 | this.setState({ play: false }); 102 | this.props.updateProps({ 103 | on: false, 104 | hover: false 105 | }); 106 | } 107 | 108 | // Receives previous props state and plays/stops audio 109 | // based on whether or not the hover prop changed values 110 | componentDidUpdate(prevProps) { 111 | if (this.props.play !== prevProps.play) { // for hovering 112 | this.playAudio(); 113 | } else if (this.props.sync !== prevProps.sync) { // for audio sync 114 | Tone.Transport.bpm.value = 78; // For audio sync 115 | Tone.Transport.bpm.rampTo(80, 14); 116 | Tone.Transport.timeSignature = [6, 8]; 117 | this.turnOn("+0.2"); // start time of audio 118 | } 119 | } 120 | 121 | render() { 122 | const { steps, hasError, idyll, updateProps, ...props } = this.props; 123 | var beat = this.state.onBeat; 124 | return [ 125 |
126 | 1 ? "6/8" : ""} /> 131 |
132 | ] 133 | } 134 | } 135 | 136 | module.exports = SixEightDemo; -------------------------------------------------------------------------------- /components/ThreeFourDemo.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | import CircleGraphic from "./CircleGraphic.js"; 3 | 4 | var Tone; 5 | var sampler; 6 | var pattern; 7 | 8 | class ThreeFourDemo extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | play: false, 13 | mounted: false, 14 | opacity: "0.8", 15 | onBeat: 0, 16 | rotation: "rotate(0 200 150)", 17 | degrees: 0 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | Tone = require("tone"); 23 | // creates it once to avoid overlapping synths 24 | sampler = new Tone.Sampler({ 25 | C4: "static/sounds/bassdrum4.wav", 26 | E4: "static/sounds/silence.wav", 27 | D4: "static/sounds/hihat3.wav" 28 | }).toMaster(); 29 | 30 | // To avoid overlapping patterns, declare here 31 | // Allows stop and start to end where it left off 32 | pattern = new Tone.Sequence( 33 | function(time, note) { 34 | this.animateCircles(note, time); 35 | sampler.triggerAttackRelease(note, 0.25); 36 | }.bind(this), 37 | ["C4", "E4", "D4", "E4", "D4", "E4"], 38 | "4n" 39 | ); 40 | 41 | // Make sure it is mounted before loading up 42 | // sampler 43 | this.setState({ mounted: true }); 44 | } 45 | 46 | // Animates the circle in sync with the current 47 | // note being played 48 | animateCircles(note, time) { 49 | Tone.Draw.schedule( 50 | function() { 51 | this.props.updateProps({ 52 | beatNum: (this.props.beatNum % 6) + 1 53 | }); 54 | 55 | this.setState({ onBeat: this.state.onBeat + 1 }); 56 | this.setState({ 57 | rotation: "rotate(" + this.state.degrees + " 200 150)" 58 | }); 59 | this.setState({ degrees: this.state.degrees + 60 }); 60 | }.bind(this), 61 | time 62 | ); 63 | } 64 | 65 | // Toggles play on and off and creates a synth 66 | // to be played. Changes the button text to 67 | // on/off 68 | playAudio() { 69 | // Play the audio when loaded and clicked 70 | if (Tone.Transport.state === "stopped") { 71 | Tone.Transport.bpm.value = 120; 72 | this.turnOn("+0"); 73 | } else if (this.state.play) { 74 | this.turnOff(); 75 | } 76 | } 77 | 78 | turnOff() { 79 | Tone.Transport.stop(); 80 | pattern.stop(); 81 | this.setState({ opacity: "0.6" }); 82 | this.setState({ play: false }); 83 | this.props.updateProps({ 84 | on: false, 85 | hover: false 86 | }); 87 | } 88 | 89 | turnOn(start) { 90 | if (this.state.mounted && !this.state.play) { 91 | this.setState({ degrees: 0 }); 92 | this.setState({ onBeat: 0 }); 93 | this.props.updateProps({ 94 | beatNum: 0 95 | }); 96 | 97 | // starts the transport and lets 98 | // us know that playback is on 99 | Tone.Transport.start(start); 100 | pattern.start(start); 101 | this.setState({ opacity: "1" }); 102 | this.setState({ play: true }); 103 | this.props.updateProps({ 104 | on: true, 105 | hover: true 106 | }); 107 | } else if (this.state.play) { 108 | this.turnOff(); 109 | } 110 | } 111 | 112 | componentDidUpdate(prevProps) { 113 | if (this.props.play !== prevProps.play) { 114 | this.playAudio(); 115 | } else if (this.props.sync !== prevProps.sync) { 116 | Tone.Transport.bpm.value = 348; // For audio sync 117 | Tone.Transport.bpm.rampTo(364, 10); 118 | Tone.Transport.timeSignature = [3, 4]; 119 | this.turnOn("+2.2"); // start time of audio 120 | } 121 | } 122 | 123 | render() { 124 | const { 125 | steps, 126 | beatNum, 127 | hasError, 128 | idyll, 129 | updateProps, 130 | ...props 131 | } = this.props; 132 | var beat = this.state.onBeat; 133 | return [ 134 |
139 | 1 ? "3/4" : ""} 153 | /> 154 |
155 | ]; 156 | } 157 | } 158 | 159 | module.exports = ThreeFourDemo; 160 | -------------------------------------------------------------------------------- /analytics/index.js: -------------------------------------------------------------------------------- 1 | 2 | var firebase = require('firebase/app'); 3 | require('firebase/auth'); 4 | require('firebase/database'); 5 | 6 | var config = { 7 | apiKey: "AIzaSyDhicO25co-cxuhvOzDang4Ws_-lZFa_dU", 8 | authDomain: "interactive-analytics-cfc22.firebaseapp.com", 9 | databaseURL: "https://interactive-analytics-cfc22-b840c.firebaseio.com/", 10 | projectId: "interactive-analytics-cfc22", 11 | storageBucket: "interactive-analytics-cfc22.appspot.com", 12 | messagingSenderId: "109430261621" 13 | }; 14 | 15 | 16 | const FLUSH_INTERVAL = 2000; 17 | 18 | /** 19 | * Called every `FLUSH_INTERVAL`ms, 20 | * this sends scroll data back to 21 | * the database. 22 | */ 23 | const getViewport = () => { 24 | let viewPortWidth; 25 | let viewPortHeight; 26 | 27 | // the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight 28 | if (typeof window.innerWidth != 'undefined') { 29 | viewPortWidth = window.innerWidth, 30 | viewPortHeight = window.innerHeight 31 | } 32 | 33 | // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document) 34 | else if (typeof document.documentElement != 'undefined' 35 | && typeof document.documentElement.clientWidth != 36 | 'undefined' && document.documentElement.clientWidth != 0) { 37 | viewPortWidth = document.documentElement.clientWidth, 38 | viewPortHeight = document.documentElement.clientHeight 39 | } 40 | 41 | // older versions of IE 42 | else { 43 | viewPortWidth = document.getElementsByTagName('body')[0].clientWidth, 44 | viewPortHeight = document.getElementsByTagName('body')[0].clientHeight 45 | } 46 | return [viewPortWidth, viewPortHeight]; 47 | } 48 | 49 | 50 | class Analytics { 51 | 52 | flushData() { 53 | this.visitRef.child('timeOnPage').set(new Date() - this.startTime); 54 | if (this.scrolls.length) { 55 | this.visitRef.child(`scroll-positions/${this.scrollCount++}`).set(JSON.stringify(this.scrolls)); 56 | this.scrolls = []; 57 | } 58 | // if (this.mousePositions.length) { 59 | // this.visitRef.child(`mouse-positions/${this.mouseCount++}`).set(JSON.stringify(this.mousePositions)); 60 | // this.mousePositions = []; 61 | // } 62 | } 63 | 64 | updateState(newState) { 65 | this.visitRef.child(`state/${this.stateCount++}`).set(JSON.stringify(Object.assign({}, newState, { 66 | timestamp: new Date() - this.startTime 67 | }))); 68 | } 69 | 70 | onLoad(cb) { 71 | this._onLoad = cb; 72 | } 73 | 74 | constructor(projectName) { 75 | this.startTime = new Date(); 76 | this.scrolls = []; 77 | // this.mousePositions = []; 78 | this.scrollCount = 0; 79 | this.mouseCount = 0; 80 | this.stateCount = 0; 81 | 82 | // window.addEventListener('mousemove', (e) => { 83 | // var t = new Date(); 84 | // var diff = t - this.startTime; 85 | // var x = e.pageX; 86 | // var y = e.pageY; 87 | // this.mousePositions.push([ diff, x, y ]); 88 | // }); 89 | 90 | var doc = document.documentElement; 91 | window.addEventListener("scroll", () => { 92 | var t = new Date(); 93 | var diff = t - this.startTime; 94 | 95 | // var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); 96 | var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); 97 | this.scrolls.push([ diff, top ]); 98 | }); 99 | 100 | var _navigator = {}; 101 | for (var i in navigator) _navigator[i] = navigator[i]; 102 | 103 | delete _navigator.plugins; 104 | delete _navigator.mimeTypes; 105 | delete _navigator.credentials; 106 | delete _navigator.clipboard; 107 | 108 | const userDetails = { 109 | location: window.location, 110 | viewport: getViewport(), 111 | navigator: _navigator, 112 | top: (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) 113 | }; 114 | 115 | firebase.initializeApp(config); 116 | 117 | firebase.auth().signInAnonymously().then(({user}) => { 118 | const userRef = firebase.database().ref(`${projectName}/users/${user.uid}`); 119 | 120 | userRef.child('visitCount').once('value').then((value) => { 121 | let v = value && value.toJSON ? value.toJSON() : 0; 122 | let visitCount = v + 1; 123 | userRef.child('visitCount').set(visitCount); 124 | userRef.child('details').set(JSON.stringify(userDetails)) 125 | 126 | this.visitRef = userRef.child(`visits/${visitCount}`); 127 | // userRef.child(`visits`).set(''+startTime); 128 | this.visitRef.child('details').set(JSON.stringify(userDetails)); 129 | this.visitRef.child('startTime').set(+this.startTime); 130 | 131 | setInterval(() => { 132 | this.flushData(); 133 | }, FLUSH_INTERVAL); 134 | 135 | this._onLoad && this._onLoad(); 136 | }); 137 | 138 | }); 139 | } 140 | } 141 | 142 | 143 | module.exports = Analytics; -------------------------------------------------------------------------------- /components/default/equation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ReactDOM = require('react-dom'); 3 | const Latex = require('react-latex-patched'); 4 | const select = require('d3-selection').select; 5 | const format = require('d3-format').format; 6 | 7 | const allowedProps = ['domain', 'step', 'children']; 8 | 9 | class Equation extends React.PureComponent { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | showRange: false 14 | }; 15 | } 16 | 17 | handleChange(event) { 18 | this.props.updateProps({ 19 | value: +event.target.value 20 | }); 21 | } 22 | 23 | componentDidMount() { 24 | let dom; 25 | 26 | const cssId = 'idyll-equation-css'; // you could encode the css path itself to generate id.. 27 | const cssURL = '//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0/katex.min.css' 28 | if (document && !document.getElementById(cssId) && !this.props.skipCSS && !select(`link[href='${cssURL}']`).size()) { 29 | const heads = document.getElementsByTagName('head') 30 | if (heads.length) { 31 | const head = heads[0]; 32 | const link = document.createElement('link'); 33 | link.id = cssId; 34 | link.href = cssURL; 35 | link.rel = 'stylesheet'; 36 | link.type = 'text/css'; 37 | link.media = 'all'; 38 | head.appendChild(link); 39 | } 40 | } 41 | 42 | try { 43 | dom = ReactDOM.findDOMNode(this); 44 | } catch(e) {}; 45 | if (!dom) { 46 | return; 47 | } 48 | 49 | this.propNodes = {}; 50 | const self = this; 51 | select(dom).selectAll('.mord').each(function (d) { 52 | const $this = select(this); 53 | Object.keys(self.props).filter((prop) => { 54 | return allowedProps.indexOf(prop) === -1 55 | }).forEach((prop) => { 56 | if ($this.text() === prop) { 57 | self.propNodes[prop] = $this; 58 | $this.style('cursor', 'pointer'); 59 | $this.on('mouseover', () => { 60 | $this.style('color', 'red'); 61 | }).on('mouseout', () => { 62 | if (!(self.state.showRange && self.state.var === prop)) { 63 | $this.style('color', 'black'); 64 | } 65 | }).on('click', () => { 66 | 67 | if (!(self.state.showRange && self.state.var === prop)) { 68 | self.setState({ 69 | showRange: true, 70 | var: prop 71 | }); 72 | $this.text(self.props[prop]) 73 | $this.style('color', 'red'); 74 | Object.keys(self.propNodes).filter(d => d !== prop).forEach((d) => { 75 | self.propNodes[d].text(d); 76 | self.propNodes[d].style('color', 'black'); 77 | }) 78 | } else { 79 | self.setState({ 80 | showRange: false, 81 | var: prop 82 | }); 83 | $this.style('color', 'black'); 84 | $this.text(prop) 85 | } 86 | }) 87 | } 88 | }) 89 | }); 90 | 91 | } 92 | 93 | handleRangeUpdate(event) { 94 | const newProps = {}; 95 | const val = +event.target.value; 96 | newProps[this.state.var] = val; 97 | this.props.updateProps(newProps); 98 | this.propNodes[this.state.var].text(val); 99 | } 100 | 101 | renderEditing() { 102 | if (!this.state.showRange) { 103 | return null; 104 | } 105 | 106 | const d = (this.props.domain || {})[this.state.var] || [-10, 10]; 107 | const step = (this.props.step || {})[this.state.var] || 0.1; 108 | return ( 109 |
110 | 111 |
112 | ); 113 | } 114 | 115 | getLatex() { 116 | if (this.props.latex) { 117 | return this.props.latex; 118 | } 119 | return (this.props.children && this.props.children[0]) ? this.props.children[0] : ''; 120 | } 121 | 122 | render() { 123 | const latexChar = '$'; 124 | const latexString = latexChar + this.getLatex() + latexChar; 125 | 126 | let style; 127 | if (this.state.showRange) { 128 | style = this.props.style; 129 | } else { 130 | style = Object.assign({ 131 | display: this.props.display ? "block" : "inline-block" 132 | }, this.props.style); 133 | } 134 | 135 | return ( 136 | 137 | {latexString} 138 | {this.renderEditing()} 139 | 140 | ); 141 | } 142 | } 143 | 144 | Equation._idyll = { 145 | name: "Equation", 146 | tagType: "open", 147 | children: "y = x^2", 148 | props: [{ 149 | name: "display", 150 | type: "boolean", 151 | example: "true" 152 | }] 153 | } 154 | 155 | export default Equation; 156 | -------------------------------------------------------------------------------- /static/images/Cursor/Pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cursor / Pointer 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 | -------------------------------------------------------------------------------- /components/default/scroller.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const { filterChildren, mapChildren } = require('idyll-component-children'); 3 | import TextContainer from './text-container'; 4 | const d3 = require('d3-selection'); 5 | 6 | 7 | const styles = { 8 | SCROLL_GRAPHIC: { 9 | position: 'absolute', 10 | top: 0, 11 | left: 0, 12 | right: 0, 13 | bottom: 'auto', 14 | height: '100vh', 15 | width: '100%', 16 | transform: `translate3d(0, 0, 0)`, 17 | zIndex: -1 18 | }, 19 | SCROLL_GRAPHIC_FIXED: { 20 | position: 'fixed' 21 | }, 22 | SCROLL_GRAPHIC_BOTTOM: { 23 | bottom: 0, 24 | top: 'auto' 25 | }, 26 | 27 | SCROLL_GRAPHIC_INNER: { 28 | position: 'absolute', 29 | // right: '1rem', 30 | left: 0, 31 | right: 0, 32 | top: '50%', 33 | transform: 'translateY(-50%)' 34 | } 35 | } 36 | 37 | let id = 0; 38 | 39 | class Scroller extends React.Component { 40 | constructor(props) { 41 | super(props); 42 | this.id = id++; 43 | this.state = { 44 | isFixed: false, 45 | isBottom: false, 46 | graphicHeight: 0, 47 | graphicWidth: 0 48 | }; 49 | 50 | this.SCROLL_STEP_MAP = {}; 51 | this.SCROLL_NAME_MAP = {}; 52 | } 53 | 54 | 55 | componentDidMount() { 56 | require('intersection-observer'); 57 | const scrollama = require('scrollama'); 58 | // instantiate the scrollama 59 | const scroller = scrollama(); 60 | this.handleResize(); 61 | 62 | // setup the instance, pass callback functions 63 | scroller 64 | .setup({ 65 | step: '.idyll-scroll-text .idyll-step', // required 66 | container: `#idyll-scroll-${this.id}`, // required (for sticky) 67 | graphic: '.idyll-scroll-graphic' // required (for sticky) 68 | }) 69 | .onStepEnter(this.handleStepEnter.bind(this)) 70 | // .onStepExit(handleStepExit) 71 | .onContainerEnter(this.handleContainerEnter.bind(this)) 72 | .onContainerExit(this.handleContainerExit.bind(this)); 73 | 74 | 75 | // setup resize event 76 | window.addEventListener('resize', this.handleResize.bind(this)); 77 | } 78 | 79 | handleStepEnter({ element, index, direction }) { 80 | this.SCROLL_STEP_MAP[index] && this.SCROLL_STEP_MAP[index](); 81 | let update = { currentStep: index }; 82 | if (this.SCROLL_NAME_MAP[index]) { 83 | update.currentState = this.SCROLL_NAME_MAP[index]; 84 | } 85 | this.props.updateProps && this.props.updateProps(update); 86 | if (index === Object.keys(this.SCROLL_STEP_MAP).length - 1) { 87 | d3.select('body').style('overflow', 'auto'); 88 | } 89 | } 90 | 91 | handleResize() { 92 | this.setState({ 93 | graphicHeight: window.innerHeight + 'px', 94 | graphicWidth: window.innerWidth + 'px', 95 | }); 96 | } 97 | handleContainerEnter(response) { 98 | if (this.props.disableScroll && (!this.props.currentStep || this.props.currentStep < Object.keys(this.SCROLL_STEP_MAP).length - 1)) { 99 | d3.select('body').style('overflow', 'hidden'); 100 | } 101 | this.setState({ isFixed: true, isBottom: false }); 102 | } 103 | 104 | handleContainerExit(response) { 105 | this.setState({ isFixed: false, isBottom: response.direction === 'down'}); 106 | } 107 | 108 | componentWillReceiveProps(nextProps) { 109 | if (this.props.currentStep !== nextProps.currentStep) { 110 | d3.selectAll(`#idyll-scroll-${this.id} .idyll-step`) 111 | .filter(function (d, i) { return i === nextProps.currentStep;}) 112 | .node() 113 | .scrollIntoView({ behavior: 'smooth' }); 114 | } 115 | if (this.props.currentState !== nextProps.currentState) { 116 | d3.selectAll(`#idyll-scroll-${this.id} .idyll-step`) 117 | .filter(function (d, i) { return nextProps.currentState === this.SCROLL_NAME_MAP[i] }) 118 | .node() 119 | .scrollIntoView({ behavior: 'smooth' }); 120 | } 121 | if (nextProps.disableScroll && (!nextProps.currentStep || nextProps.currentStep < Object.keys(this.SCROLL_STEP_MAP).length - 1)) { 122 | d3.select('body').style('overflow', 'hidden'); 123 | } 124 | } 125 | 126 | registerStep(elt, name, val) { 127 | this.SCROLL_STEP_MAP[elt] = val; 128 | this.SCROLL_NAME_MAP[elt] = name; 129 | } 130 | 131 | render() { 132 | const { hasError, updateProps, idyll, children, ...props } = this.props; 133 | const { isFixed, isBottom, graphicHeight, graphicWidth } = this.state; 134 | return ( 135 |
this.ref = ref} className="idyll-scroll" id={`idyll-scroll-${this.id}`} style={{position: 'relative'}}> 136 |
141 | 142 |
143 | {filterChildren( 144 | children, 145 | (c) => { 146 | return c.type.name && c.type.name.toLowerCase() === 'graphic'; 147 | } 148 | )} 149 |
150 |
151 | 152 |
153 | {mapChildren(filterChildren( 154 | children, 155 | (c) => { 156 | return !c.type.name || c.type.name.toLowerCase() === 'step'; 157 | } 158 | ), (c) => { 159 | return React.cloneElement(c, { 160 | registerStep: this.registerStep.bind(this) 161 | }); 162 | })} 163 |
164 |
165 |
166 | ); 167 | } 168 | } 169 | 170 | export default Scroller; 171 | -------------------------------------------------------------------------------- /components/LinearBeats.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | var Tone; 4 | var pattern; 5 | var sampler; 6 | 7 | const MAIN_BEAT = "#FF851B"; 8 | const UNSTRESSED = "#EDAE49"; 9 | const STRESSED_OFFBEAT = "#087E8B"; 10 | 11 | class LinearBeats extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | play: false, 16 | mounted: false, 17 | text: "Start Audio", 18 | opacity: "0.8", 19 | onBeat: 0 20 | }; 21 | } 22 | 23 | componentDidMount() { 24 | Tone = require('tone'); 25 | // creates it once to avoid overlapping synths 26 | sampler = new Tone.Sampler({ 27 | "E4": "static/sounds/hihat3.wav", 28 | }).toMaster(); 29 | 30 | // To avoid overlapping patterns, declare here 31 | // Allows stop and start to end where it left off 32 | pattern = new Tone.Sequence(function (time, note) { 33 | this.animateCircles(note, time); 34 | sampler.triggerAttackRelease(note, .25); 35 | }.bind(this), ["E4", "E4", "E4", "E4", "E4", "E4"], "4n"); 36 | 37 | // Make sure it is mounted before loading up 38 | // sampler 39 | this.setState({ mounted: true }); 40 | } 41 | 42 | // Animates the circle in sync with the current 43 | // note being played 44 | animateCircles(note, time) { 45 | Tone.Draw.schedule(function () { 46 | this.props.updateProps({ 47 | beatNum: ((this.props.beatNum) % 6) + 1 48 | }); 49 | console.log(this.props.beatCount); 50 | this.setState({ onBeat: this.state.onBeat + 1 }); 51 | }.bind(this), time); 52 | } 53 | 54 | // Function for time -> Angle 55 | 56 | // Toggles play on and off and creates a synth 57 | // to be played. Changes the button text to 58 | // on/off 59 | playAudio() { 60 | this.setState({ onBeat: -1 }); // reset each time 61 | Tone.Transport.bpm.value = 120; 62 | // Play the audio when loaded and clicked and the transport isn't playing anything 63 | if (this.state.mounted && !this.state.play && Tone.Transport.state === "stopped") { 64 | this.props.updateProps({ 65 | beatNum: 0 66 | }); 67 | this.setState({ degrees: 0 }); 68 | this.setState({ onBeat: 0 }); 69 | 70 | // starts the transport and lets 71 | // us know that playback is on 72 | Tone.Transport.start(); 73 | pattern.start(0); 74 | this.setState({ play: true }); 75 | } else if (this.state.play) { 76 | // Stops transport and lets us know 77 | // playback is free to start playing 78 | // the next thing 79 | turnOff(); 80 | } 81 | document.getElementById("ptr").classList.add("hide"); 82 | } 83 | 84 | turnOff() { 85 | Tone.Transport.stop(); 86 | pattern.stop(); 87 | this.setState({ play: false }); 88 | document.getElementById("ptr").classList.remove("hide"); 89 | } 90 | 91 | // Displays the strong vs weak text based on what is hovered over 92 | showText() { 93 | var result; 94 | if (this.props.displayThreeFour && this.props.mode !== 1) { 95 | var strong = Strong; 96 | var weak1 = Weak; 97 | var weak2 = Weak; 98 | result = [strong, weak1, weak2]; 99 | } else if (this.props.displaySixEight && this.props.mode !== 0) { 100 | var strongest = Strongest; 101 | var weak1 = Weak; 102 | var weak2 = Weak; 103 | var strong = Strong; 104 | var weak4 = Weak; 105 | var weak5 = Weak; 106 | result = [strongest, weak1, weak2, strong, weak4, weak5]; 107 | } 108 | return result; 109 | } 110 | 111 | 112 | render() { 113 | const { displayThreeFour, displaySixEight, play, beatCount, mode, hasError, idyll, updateProps, ...props } = this.props; 114 | var beat = mode === 2 ? this.state.onBeat : beatCount; // later switch to ternary when using props 115 | var display = displayThreeFour || displaySixEight; 116 | var validDisplay1 = displayThreeFour && mode !== 1; // only display when mode corresponds correctly 117 | var validDisplay2 = displaySixEight && mode !== 0; // only display when mode is 1 or 2 118 | return ( 119 |
120 | 124 | 125 | 127 | 129 | 131 | 133 | 135 | 136 | 137 | {display ? this.showText() : () => { }} 138 | 139 | 140 |
141 | ) 142 | } 143 | } 144 | 145 | module.exports = LinearBeats; -------------------------------------------------------------------------------- /static/images/Pointer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pointer 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.idyll: -------------------------------------------------------------------------------- 1 | [meta title:"Beat Basics: 3/4 and 6/8" description:"Differentiating 3/4 and 6/8" /] 2 | 3 | [var name:"step" value:0 /] // what scroller view we are in 4 | [var name:"demoNumOn" value:`false ` /] // numbering visual 5 | [var name:"beatNumThreeFour" value:0 /] // keep track of curr beat 6 | [var name:"beatNumSixEight" value:0 /] // keep track of curr beat 7 | [var name:"beatNumDefault" value:0 /] // keeps track of default beat 8 | [var name:"beatHoverThree" value:`false ` /] // hover state of 3/4 9 | [var name:"beatHoverSix" value:`false `/] // hover state of 6/8 10 | 11 | // Strong/weak visual emphasis based on time sig 12 | [var name:"emphasizeThreeFour" value:`false `/] 13 | [var name:"emphasizeSixEight" value:`false `/] 14 | 15 | // True when visual is playing. False if not 16 | [var name:"threeFourOn" value:`false `/] 17 | [var name:"sixEightOn" value:`false `/] 18 | [var name:"playLinear" value:`false `/] 19 | 20 | [Scroller currentStep:step] 21 | [Step] 22 | [Header 23 | title:"BEAT BASICS" 24 | subtitle:"An interactive exploration differentiating 3/4 and 6/8 using John Varney's rhythm wheel" 25 | author:"Megan Vo" /] 26 | 27 | [Unmute /] 28 | [/Step] 29 | 30 | [Step] 31 | // ### INTRODUCTION 32 | The **two grey circles** on this page each represent a **rhythm**. 33 | Go ahead and **hover** over them to play a rhythm one at a time. 34 | They don't sound the same, do they? 35 | 36 | Intuitively, we may know that they _are_ different by picking up a few visual or aural cues. 37 | For example, the **number** and **positioning** of the circles are different for each rhythm, 38 | and the **beats** corresponding with the circles aren't at the same place. 39 | 40 | Parsing out these differences perhaps isn't the most difficult task 41 | for us to do at a high level, so let's break it down a bit more. 42 | [/Step] 43 | 44 | [Step] 45 | // This one introduces accenting/emphasizing beats 46 | ### THE BASICS 47 | In a [TedEd video](https://ed.ted.com/lessons/a-different-way-to-visualize-rhythm-john-varney), John Varney defines 48 | rhythm as "essentially an event repeating regularly over time". But as the video explains, a uniform event over time doesn't 49 | really lend itself to the more complex musical rhythms that we have on the right. Consider 50 | a simple cycle divided into 6 beats: 51 | 52 | [LinearBeats mode:`threeFourOn ? 0 : sixEightOn ? 1 : 2 ` 53 | beatCount:`threeFourOn ? beatNumThreeFour : sixEightOn ? beatNumSixEight : 0 ` 54 | beatNum:beatNumDefault displayThreeFour:emphasizeThreeFour displaySixEight:emphasizeSixEight /] 55 | 56 | 57 | In order for us to get this strand to resemble our two musical rhythms, 58 | we have to assign **stresses or accents** to the beats and figure out when to place a beat. Try hovering 59 | over the two right rhythms again. 60 | 61 | All we've really done is assign the **strongest emphasis** to the **first beat** and 62 | a weaker emphasis to the others. 63 | Delegating emphasis to beats within that six-beat cycle gives us the basic foundations of 64 | what we call [Hoverable word:`"3/4"` display:emphasizeThreeFour hover:beatHoverThree/] 65 | time signature (top rhythm) and [Hoverable word:`"6/8"` display:emphasizeSixEight hover:beatHoverSix /] (bottom rhythm). 66 | [/Step] 67 | 68 | [Step state:"reset"] 69 | // Counting and dividing the circle 70 | ### BREAKING IT DOWN 71 | One of the easiest ways to differentiate the two is by counting. 72 | 73 | For the **3/4** time signature, we can keep track of the cycle by repeating: 74 | [br/] 75 | 76 | [var name:"altThree" value:`false ` /] 77 | [BeatCount beatCount:beatNumThreeFour upTo:3 hover:beatHoverThree altShow:altThree /] 78 | 79 | [button onClick:`altThree = !altThree `][Display value:`altThree ? "Hide Alternative" : "Show Alternative" `/][/button] 80 | [br /] 81 | 82 | The "2" and "3" represent the weaker beats (or commonly, upbeats) whereas 83 | the "and"s allow us to subdivide the rhythm to better illustrate how we 84 | are dividing the circle. In this case, we are splitting the cycle into **three 85 | groups of two.** 86 | [br/] 87 | 88 | [p] 89 | Similarly, we can also break up the **6/8** rhythm into groups. 90 | When you hover 91 | over the bottom rhythm, our repeating phrase will be: 92 | [/p] 93 | 94 | [var name:"altSix" value:`false ` /] 95 | [BeatCount beatCount:beatNumSixEight upTo:2 hover:beatHoverSix altShow:altSix /] 96 | [button onClick:`altSix = !altSix `][Display value:`altSix ? "Hide Alternative" : "Show Alternative" `/][/button] 97 | 98 | 99 | This rhythm lets us split the circle into **two groups of three** which elongates the phrasing a bit. 100 | This simple variance in partitioning the circle gave us an entirely different 101 | rhythm and feel. Neat, right? 102 | 103 | Note that for **6/8**, it is also common to count numerically from 1 to 6 (see alternative counting). 104 | For this type of counting, an emphasis on the first and fourth beat signifies a **6/8** 105 | time signature, whereas a strong emphasis on the first and weaker emphasis on the third and fifth beats signal **3/4**. 106 | 107 | [/Step] 108 | 109 | [Step] 110 | ### LET'S LISTEN! 111 | The best way to get a feel for the two is to see -- or rather, hear -- 112 | them in action. 113 | 114 | Both time signatures are common in songs you've probably heard. 115 | For this introduction however, we'll be listening to Tchaikovsky's *Waltz of the Flowers* for **3/4** 116 | and Mozart's *Piano Concerto No. 23 Mvmt. 2* for **6/8**. Note that changes in [a href:"https://en.wikipedia.org/wiki/Tempo"]tempo[/a] 117 | will speed up or slow down our rhythms. 118 | 119 | The numbers are still up 120 | on our circles to help with counting. Go ahead and hover over the audio clips! 121 | See if you can hear differences in the phrasing of notes in the two cycles. 122 | 123 | [var name:"exampleSix" value:`false `/] 124 | [var name:"exampleThree" value:`false `/] 125 | [br/] 126 | 127 | [div id:"audioSource"] 128 | [div id:"audioSourceThree"] 129 | [AudioPlayer sync:exampleThree file:"waltz.mp3" /] 130 | [/div] 131 | [div id:"audioSourceSix"] 132 | [AudioPlayerSix sync:exampleSix file:"mozart.mp3" /] 133 | [/div] 134 | [/div] 135 | 136 | 137 | [/Step] 138 | 139 | [Step] 140 | ### WHY STOP HERE? 141 | 142 | This was only an introductory, surface level exploration of the two rhythms, but there is a lot more to dive into, such as 143 | what the numbers in time signatures mean. 144 | 145 | Hopefully, 146 | it helped improve intuition on how to differentiate and recognize the two at a basic level. However, there are 147 | many more nuances to the rhythms and many different kinds of rhythms to explore as well. 148 | 149 | Don't know where to start? I recommend taking a peek [a href:"https://www.khanacademy.org/humanities/music/music-basics2/notes-rhythm/v/lesson-2-rhythm-dotted-notes-ties-and-rests"]here[/a] 150 | for Khan Academy's introductory series to rhythm in general. 151 | [/Step] 152 | 153 | [Step] 154 | ### References and Appreciation 155 | * This post was created using [a href:"https://tonejs.github.io/"]Tone.js[/a] and [a href:"https://idyll-lang.org/"]Idyll[/a], an open-source markup language created by Matthew Conlen 156 | * Thanks to Noelle Vo for recording Mozart's *Piano Concerto* at a moment's notice 157 | * Super special thanks to Matt Conlen for guidance, mentorship, and feedback 158 | 159 | 1. Varney, John. “A Different Way to Visualize Rhythm - John Varney.” Lessons Worth Sharing | TED-Ed, TED-Ed, 20 Oct. 2014, ed.ted.com/lessons/a-different-way-to-visualize-rhythm-john-varney. 160 | 2. Joutsenvirta, Aarre, and Jari Perkiömäki. “Time Signatures.” Music Theory – Consonance and Dissonance, www2.siba.fi/muste1/index.php?id=98&la=en. 161 | 3. “Simple and Compound Meter.” Musictheory.net, www.musictheory.net/lessons/15. 162 | [/Step] 163 | 164 | [/Scroller] 165 | 166 | // Steps keeps track of what state the scroller is in 167 | [Fixed] 168 | [ThreeFourDemo steps:step beatNum:beatNumThreeFour on:threeFourOn play:beatHoverThree hover:emphasizeThreeFour sync:exampleThree /] 169 | [SixEightDemo steps:step beatNum:beatNumSixEight on:sixEightOn play:beatHoverSix hover:emphasizeSixEight sync:exampleSix /] 170 | [/Fixed] --------------------------------------------------------------------------------