├── now.json
├── .babelrc
├── babel-preset.js
├── static
└── sounds
│ ├── claves.wav
│ ├── cowbell.wav
│ ├── cymbal.wav
│ ├── hi_tom.wav
│ ├── low_tom.wav
│ ├── maracas.wav
│ ├── mid_tom.wav
│ ├── bass_drum.wav
│ ├── cl_hi_hat.wav
│ ├── hand_clap.wav
│ ├── hi_conga.wav
│ ├── low_conga.wav
│ ├── mid_conga.wav
│ ├── o_hi_hat.wav
│ ├── rim_shot.wav
│ └── snare_drum.wav
├── .gitignore
├── lib
├── hydrate.js
└── theme.js
├── pages
├── index.js
└── _document.js
├── readme.md
├── next.config.js
├── components
├── box
│ └── index.js
└── DrumMachine.js
└── package.json
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "alias": "tuile",
3 | "forwardNpm": true
4 | }
5 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "next/babel",
4 | "./babel-preset.js"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/babel-preset.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [[require('babel-plugin-emotion')]]
3 | }
4 |
--------------------------------------------------------------------------------
/static/sounds/claves.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/claves.wav
--------------------------------------------------------------------------------
/static/sounds/cowbell.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/cowbell.wav
--------------------------------------------------------------------------------
/static/sounds/cymbal.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/cymbal.wav
--------------------------------------------------------------------------------
/static/sounds/hi_tom.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/hi_tom.wav
--------------------------------------------------------------------------------
/static/sounds/low_tom.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/low_tom.wav
--------------------------------------------------------------------------------
/static/sounds/maracas.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/maracas.wav
--------------------------------------------------------------------------------
/static/sounds/mid_tom.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/mid_tom.wav
--------------------------------------------------------------------------------
/static/sounds/bass_drum.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/bass_drum.wav
--------------------------------------------------------------------------------
/static/sounds/cl_hi_hat.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/cl_hi_hat.wav
--------------------------------------------------------------------------------
/static/sounds/hand_clap.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/hand_clap.wav
--------------------------------------------------------------------------------
/static/sounds/hi_conga.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/hi_conga.wav
--------------------------------------------------------------------------------
/static/sounds/low_conga.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/low_conga.wav
--------------------------------------------------------------------------------
/static/sounds/mid_conga.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/mid_conga.wav
--------------------------------------------------------------------------------
/static/sounds/o_hi_hat.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/o_hi_hat.wav
--------------------------------------------------------------------------------
/static/sounds/rim_shot.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/rim_shot.wav
--------------------------------------------------------------------------------
/static/sounds/snare_drum.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tkh44/emotion-beats/master/static/sounds/snare_drum.wav
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # build output
7 | .next
8 | package-lock.json
9 |
--------------------------------------------------------------------------------
/lib/hydrate.js:
--------------------------------------------------------------------------------
1 | import { hydrate } from 'emotion'
2 |
3 | // Adds server generated styles to emotion cache.
4 | // Has to run before any `style()` calls
5 | // '__NEXT_DATA__.ids' is set in '_document.js'
6 | if (typeof window !== 'undefined') {
7 | hydrate(window.__NEXT_DATA__.ids)
8 | }
9 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import '../lib/hydrate'
2 | import dynamic from 'next/dynamic'
3 |
4 | const DrumMachine = dynamic(import('../components/DrumMachine'), {
5 | ssr: false // don't want to deal with "window" problems when loading the audio api
6 | })
7 |
8 | export default () =>
9 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 | # Emotion Beats
3 |
4 | Example for [emotion](https://github.com/emotion-js/emotion) with [next](https://github.com/zeit/next.js) and [React](https://github.com/facebook/react)
5 |
6 | #### Installation
7 | ```bash
8 | $ npm install
9 | ```
10 |
11 | #### Development
12 | ```bash
13 | $ npm run dev
14 | ```
15 |
16 | #### Production
17 | ```bash
18 | $ npm run build && npm run start
19 | ```
20 |
21 |
22 |
23 | **inspired by https://github.com/siggy/beatboxer**
24 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack: (config, { dev }) => {
3 | config.devtool = 'eval'
4 | config.module.rules.push(
5 | {
6 | test: /\.(css|scss)/,
7 | loader: 'emit-file-loader',
8 | options: {
9 | name: 'dist/[path][name].[ext]'
10 | }
11 | },
12 | {
13 | test: /\.css$/,
14 | use: ['babel-loader', 'raw-loader', 'style-loader', 'css-loader']
15 | }
16 | )
17 | return config
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/components/box/index.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import { space, width, fontSize, color, responsiveStyle } from 'styled-system'
3 |
4 |
5 | export const display = responsiveStyle('display')
6 | export const flex = responsiveStyle('flex')
7 | export const order = responsiveStyle('order')
8 | const wrap = responsiveStyle('flex-wrap', 'wrap', 'wrap')
9 | const direction = responsiveStyle('flexDirection', 'direction')
10 | const align = responsiveStyle('alignItems', 'align')
11 | const justify = responsiveStyle('justifyContent', 'justify')
12 | const column = props => props.column ? css`flex-direction:column;` : null
13 |
14 | const Box = styled('div')`
15 | ${display};
16 | ${space};
17 | ${width};
18 | ${fontSize};
19 | ${color};
20 | ${flex};
21 | ${order};
22 | ${wrap};
23 | ${column};
24 | ${direction};
25 | ${align};
26 | ${justify};
27 | `
28 |
29 | export default Box
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pivot",
3 | "private": true,
4 | "dependencies": {
5 | "babel-plugin-emotion": "^8.0.2-7",
6 | "data-driven-motion": "^0.0.11",
7 | "emotion": "^8.0.2-9",
8 | "emotion-server": "^8.0.2-9",
9 | "next": "^3.0.6",
10 | "open-color": "^1.5.1",
11 | "react": "^15.6.1",
12 | "react-dom": "^15.6.1",
13 | "react-emotion": "^8.0.2-9",
14 | "simple-listen": "^1.1.2",
15 | "styled-system": "^1.0.0-13",
16 | "whatwg-fetch": "^2.0.3"
17 | },
18 | "devDependencies": {
19 | "babel-eslint": "^7.2.3",
20 | "css-loader": "^0.28.5",
21 | "json-server": "^0.12.0",
22 | "postcss-loader": "^2.0.6",
23 | "raw-loader": "^0.5.1",
24 | "standard": "^10.0.3",
25 | "style-loader": "^0.18.2"
26 | },
27 | "scripts": {
28 | "dev": "next",
29 | "build": "next build",
30 | "start": "NODE_ENV=production next start",
31 | "lint": "standard"
32 | },
33 | "eslintConfig": {
34 | "extends": "standard",
35 | "parser": "babel-eslint",
36 | "rules": {
37 | "jsx-quotes": [
38 | "error",
39 | "prefer-single"
40 | ]
41 | }
42 | },
43 | "standard": {
44 | "parser": "babel-eslint",
45 | "ignore": [
46 | "/dist/"
47 | ]
48 | },
49 | "browserslist": [
50 | "last 1 Chrome version"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/lib/theme.js:
--------------------------------------------------------------------------------
1 | import { css, fontFace } from 'emotion'
2 | import colors from 'open-color'
3 | import { constants } from 'styled-system'
4 |
5 | const { breakpoints, space, fontSizes } = constants
6 |
7 | fontFace`
8 | font-family: 'Montserrat';
9 | font-style: normal;
10 | font-weight: 400;
11 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://fonts.gstatic.com/s/montserrat/v10/zhcz-_WihjSQC0oHJ9TCYAzyDMXhdD8sAj6OAJTFsBI.woff2) format('woff2');
12 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
13 | `
14 | fontFace`
15 | font-family: 'Montserrat';
16 | font-style: normal;
17 | font-weight: 300;
18 | src: local('Montserrat Light'), local('Montserrat-Light'), url(https://fonts.gstatic.com/s/montserrat/v10/IVeH6A3MiFyaSEiudUMXEweOulFbQKHxPa89BaxZzA0.woff2) format('woff2');
19 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
20 | `
21 |
22 | const utils = {
23 | aspectRatio: (width, height) => css`
24 | position: relative;
25 | &:before {
26 | display: block;
27 | content: "";
28 | width: 100%;
29 | padding-top: ${height / width * 100}%;
30 | }
31 | & > .aspect-ratio-content {
32 | position: absolute;
33 | top: 0;
34 | left: 0;
35 | right: 0;
36 | bottom: 0;
37 | }
38 | `,
39 | hoverStyles: css`
40 | cursor: pointer;
41 | &:hover {
42 | color: ${colors.green[5]};
43 | }
44 | `
45 | }
46 |
47 | export default {
48 | bp: breakpoints,
49 | colors,
50 | fontSizes,
51 | space,
52 | utils
53 | }
54 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript } from 'next/document'
2 | import { extractCritical } from 'emotion-server'
3 | import { injectGlobal } from 'emotion'
4 |
5 | import theme from '../lib/theme'
6 |
7 | injectGlobal`
8 | html, body {
9 | font-family: 'Montserrat',
10 | -apple-system,
11 | BlinkMacSystemFont,
12 | "Segoe UI",
13 | "Roboto",
14 | "Roboto Light",
15 | "Oxygen",
16 | "Ubuntu",
17 | "Cantarell",
18 | "Fira Sans",
19 | "Droid Sans",
20 | "Helvetica Neue",
21 | sans-serif,
22 | "Apple Color Emoji",
23 | "Segoe UI Emoji",
24 | "Segoe UI Symbol";
25 | color: ${theme.colors.gray[8]};
26 | width: 100%;
27 | height: 100%;
28 | padding: 0;
29 | margin: 0;
30 | overflow: hidden;
31 | -webkit-font-smoothing: antialiased;
32 | -moz-osx-font-smoothing: grayscale;
33 | }
34 |
35 | body > div:first-of-type,
36 | #app,
37 | #__next,
38 | #__next > div {
39 | display: flex;
40 | flex: 1;
41 | height: 100%;
42 | }
43 |
44 | * {
45 | box-sizing: border-box;
46 | }
47 |
48 | button {
49 | text-decoration: none;
50 | text-align: center;
51 | cursor: pointer;
52 | user-select: none;
53 | border: none;
54 | background: none;
55 | font-size: ${theme.fontSizes[1]};
56 | margin: 0;
57 | padding: 0;
58 |
59 | &:hover,
60 | &:active {
61 | outline: none;
62 | }
63 | }
64 |
65 | a {
66 | color: ${theme.colors.grape[5]};
67 | text-decoration: none;
68 | cursor: pointer;
69 | &:hover {
70 | color: ${theme.colors.grape[8]};
71 | }
72 | }
73 | `
74 |
75 | export default class MyDocument extends Document {
76 | static getInitialProps ({ renderPage }) {
77 | const page = renderPage()
78 | const styles = extractCritical(page.html)
79 | return { ...page, ...styles }
80 | }
81 |
82 | constructor (props) {
83 | super(props)
84 | const { __NEXT_DATA__, ids } = props
85 | if (ids) {
86 | __NEXT_DATA__.ids = this.props.ids
87 | }
88 | }
89 |
90 | render () {
91 | return (
92 |
93 |
94 | Beats with emotion
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | )
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/components/DrumMachine.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch'
2 | import '../lib/hydrate'
3 | import React from 'react'
4 | import listen from 'simple-listen'
5 |
6 | import Box from '../components/box'
7 | import theme from '../lib/theme'
8 |
9 | import { css } from 'react-emotion'
10 | const AudioContext = window.AudioContext || window.webkitAudioContext
11 | const audioCtx = new AudioContext()
12 |
13 | // adapted from https://paulbakaus.com/tutorials/html5/web-audio-on-ios/
14 | function enableIOSAudio () {
15 | const buffer = audioCtx.createBuffer(1, 1, 22050)
16 | const source = audioCtx.createBufferSource()
17 |
18 | source.buffer = buffer
19 | source.connect(audioCtx.destination)
20 | source.noteOn(0)
21 |
22 | window.removeEventListener('touchend', enableIOSAudio, false)
23 | }
24 |
25 | window.addEventListener('touchend', enableIOSAudio, false)
26 |
27 | function fileReaderReady (reader) {
28 | return new Promise(function (resolve, reject) {
29 | reader.onload = function () {
30 | resolve(reader.result)
31 | }
32 | reader.onerror = function () {
33 | reject(reader.error)
34 | }
35 | })
36 | }
37 |
38 | function readBlobAsArrayBuffer (blob) {
39 | const reader = new window.FileReader()
40 | const promise = fileReaderReady(reader)
41 | reader.readAsArrayBuffer(blob)
42 | return promise
43 | }
44 |
45 | // sounds originated from http://808.html909.com
46 | const sounds = [
47 | 'hand_clap.wav',
48 | 'snare_drum.wav',
49 | 'mid_tom.wav',
50 | 'low_tom.wav',
51 | 'o_hi_hat.wav',
52 | 'mid_conga.wav',
53 | 'low_conga.wav',
54 | 'bass_drum.wav'
55 | ]
56 |
57 | const BPM = 90
58 | const COLUMNS = 16
59 | const INTERVAL = 1 / (4 * BPM / (60 * 1000))
60 |
61 | export default class DrumMachine extends React.Component {
62 | static displayName = 'Index Page'
63 |
64 | constructor (props) {
65 | super(props)
66 | this.state = {
67 | loading: true,
68 | grid: Array.from({ length: 8 * 16 }, () => false)
69 | }
70 | this.buffers = []
71 | this.lastCol = COLUMNS - 1
72 | this.currentCol = 0
73 | this.lastTime = new Date().getTime()
74 | }
75 |
76 | async componentWillMount () {
77 | this.buffers = await Promise.all(
78 | sounds.map(
79 | sound =>
80 | new Promise((resolve, reject) => {
81 | const req = new window.XMLHttpRequest()
82 | req.open('GET', `/static/sounds/${sound}`, true)
83 | req.responseType = 'arraybuffer'
84 |
85 | req.onload = function () {
86 | const data = req.response
87 |
88 | audioCtx.decodeAudioData(
89 | data,
90 | buffer => {
91 | resolve(buffer)
92 | },
93 | err => reject(err)
94 | )
95 | }
96 | req.send()
97 | })
98 | )
99 | )
100 | this.hashListener = listen(window, 'hashchange', this.handleHashChange)
101 | this.handleHashChange()
102 | this.setState({ loaded: true }, () => {
103 | this.currentFrame = window.requestAnimationFrame(this.loop)
104 | })
105 | }
106 |
107 | componentDidMount () {}
108 |
109 | componentWillUnmount () {
110 | window.cancelAnimationFrame(this.currentFrame)
111 | }
112 |
113 | handleHashChange = () => {
114 | let substr = window.location.hash.substr(1).padEnd(8 * 16, '0')
115 | const outGrid = [...substr].map((flag, i) => {
116 | return flag === '1'
117 | })
118 |
119 | console.log(outGrid)
120 |
121 | this.setState({ grid: outGrid })
122 | }
123 |
124 | loop = () => {
125 | const now = new Date().getTime()
126 |
127 | if (now - this.lastTime >= INTERVAL) {
128 | this.state.grid.forEach((isActive, i) => {
129 | const col = i % 16
130 | if (isActive && this.currentCol === col) {
131 | const row = i < 16 ? 0 : Math.floor(i / 16)
132 | try {
133 | const source = audioCtx.createBufferSource()
134 | source.buffer = this.buffers[row]
135 | source.connect(audioCtx.destination)
136 | source.start(0)
137 | } catch (e) {
138 | console.error(e.message)
139 | }
140 | }
141 | })
142 |
143 | this.lastCol = this.currentCol
144 | this.currentCol = (this.currentCol + 1) % COLUMNS
145 | this.lastTime = now
146 | this.setState({ currentCol: this.currentCol })
147 | }
148 |
149 | this.currentFrame = window.requestAnimationFrame(this.loop)
150 | }
151 |
152 | render () {
153 | return (
154 |
164 | {Array.from({ length: 8 * 16 }).map((blank, i) => {
165 | const col = i % 16
166 |
167 | return (
168 | {
194 | const hash = window.location.hash.substr(1).padEnd(8 * 16, '0')
195 |
196 | window.location.hash =
197 | hash.substr(0, i) +
198 | (hash.charAt(i) === '1' ? '0' : '1') +
199 | hash.substr(i + 1)
200 | }}
201 | />
202 | )
203 | })}
204 |
205 | )
206 | }
207 | }
208 |
--------------------------------------------------------------------------------