├── config ├── jest │ ├── CSSStub.js │ └── FileStub.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── src ├── files │ ├── files-list.css │ ├── file.css │ ├── file.js │ ├── drag-and-drop-receiver.js │ └── files-list.js ├── lib │ ├── regular-expressions.js │ ├── audio │ │ └── context.js │ ├── store.js │ ├── beatclock.js │ ├── video │ │ ├── dot-matrix-shader.js │ │ └── renderer.js │ ├── audio-graph.js │ ├── junk.js │ ├── loader.js │ ├── files.js │ ├── midi.js │ └── scheduler.js ├── tracks │ ├── track-editor.css │ ├── tracks.js │ └── track-editor.js ├── pad │ ├── pad.css │ ├── pads.css │ ├── pad-editor.css │ ├── pads.js │ ├── pad.js │ └── pad-editor.js ├── settings │ ├── settings.css │ └── settings.js ├── video-controls-gui.css ├── clip │ ├── clip-editor.css │ ├── clip.js │ ├── clip.css │ ├── play-audio-clip.js │ └── clip-editor.js ├── video-player.css ├── live-mode.css ├── styles │ └── forms.css ├── editor.css ├── data │ ├── file-loader.js │ ├── settings.js │ ├── reducer.js │ ├── video-renderer.js │ ├── controllers.js │ ├── files.js │ ├── tracks.js │ ├── blank_project.json │ ├── scheduler.js │ ├── clips.js │ └── pads.js ├── index.css ├── projects.css ├── index.js ├── live-mode.js ├── video-controls-gui.js ├── video-player.js ├── editor.js ├── projects.js └── vendor │ └── web-midi.js ├── public ├── favicon.ico ├── initial │ ├── 1.mp3 │ ├── 2.mp3 │ ├── 3.mp3 │ ├── 4.mp3 │ ├── 5.mp3 │ ├── 6.mp3 │ ├── 7.mp3 │ ├── 8.mp3 │ ├── mysound.mp3 │ ├── mysound2.mp3 │ └── project.json └── index.html ├── .gitignore ├── scripts ├── test.js ├── build-electron.js ├── start-electron.js ├── build.js └── start.js ├── README.md └── package.json /config/jest/CSSStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /config/jest/FileStub.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; 2 | -------------------------------------------------------------------------------- /src/files/files-list.css: -------------------------------------------------------------------------------- 1 | .filesList { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/initial/1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/1.mp3 -------------------------------------------------------------------------------- /public/initial/2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/2.mp3 -------------------------------------------------------------------------------- /public/initial/3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/3.mp3 -------------------------------------------------------------------------------- /public/initial/4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/4.mp3 -------------------------------------------------------------------------------- /public/initial/5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/5.mp3 -------------------------------------------------------------------------------- /public/initial/6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/6.mp3 -------------------------------------------------------------------------------- /public/initial/7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/7.mp3 -------------------------------------------------------------------------------- /public/initial/8.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/8.mp3 -------------------------------------------------------------------------------- /public/initial/mysound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/mysound.mp3 -------------------------------------------------------------------------------- /public/initial/mysound2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nestedloops/jsconf-2017/HEAD/public/initial/mysound2.mp3 -------------------------------------------------------------------------------- /src/lib/regular-expressions.js: -------------------------------------------------------------------------------- 1 | export const isAudio = /(mp3|wav|ogg)/; 2 | export const isVideo = /(mp4|mpg)/; 3 | -------------------------------------------------------------------------------- /src/tracks/track-editor.css: -------------------------------------------------------------------------------- 1 | .trackEditor__name, 2 | .trackEditor__nameInput { 3 | margin: 10px 0; 4 | font-size: 18px; 5 | } 6 | -------------------------------------------------------------------------------- /src/pad/pad.css: -------------------------------------------------------------------------------- 1 | .pad__row { 2 | display: flex; 3 | margin-bottom: 1px; 4 | } 5 | 6 | .pad__row:last-child { 7 | margin-bottom: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/settings/settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 10px; 3 | } 4 | 5 | .settings__button { 6 | margin: 10px 0; 7 | display: block; 8 | } 9 | -------------------------------------------------------------------------------- /src/video-controls-gui.css: -------------------------------------------------------------------------------- 1 | .gui-container { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | } 6 | 7 | .gui-container .dg .slider { 8 | width: 60%; 9 | } -------------------------------------------------------------------------------- /src/clip/clip-editor.css: -------------------------------------------------------------------------------- 1 | .clipEditor { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .editorForm__selectTrack { 8 | flex-grow: 1; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/audio/context.js: -------------------------------------------------------------------------------- 1 | const AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext; 2 | const context = new AudioContext(); 3 | 4 | export default context; 5 | -------------------------------------------------------------------------------- /src/video-player.css: -------------------------------------------------------------------------------- 1 | .simpleVideoContainer { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | } 8 | 9 | .simpleVideoContainer video { 10 | min-width: 100%; 11 | min-height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | electron-build 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /src/pad/pads.css: -------------------------------------------------------------------------------- 1 | .pads { 2 | display: flex; 3 | flex-direction: column; 4 | padding-top: 10px; 5 | } 6 | 7 | .pads__addPad { 8 | position: absolute; 9 | right: 50px; 10 | width: 100px; 11 | } 12 | 13 | .pads__padContainer { 14 | margin-bottom: 30px; 15 | } 16 | -------------------------------------------------------------------------------- /src/live-mode.css: -------------------------------------------------------------------------------- 1 | .liveMode { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: -1; 8 | background-color: #000; 9 | opacity: 0; 10 | transition: opacity 0.2s ease-in-out; 11 | } 12 | 13 | .liveMode.m-visible { 14 | opacity: 1; 15 | z-index: 1; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/store.js: -------------------------------------------------------------------------------- 1 | import { /*applyMiddleware,*/ createStore } from 'redux'; 2 | // import createLogger from 'redux-logger'; 3 | import reducer from '../data/reducer'; 4 | 5 | export default createStore( 6 | reducer, 7 | {} 8 | // use for debugging 9 | // ,applyMiddleware( 10 | // createLogger({collapsed: true }) 11 | // ) 12 | ); -------------------------------------------------------------------------------- /src/styles/forms.css: -------------------------------------------------------------------------------- 1 | .editorForm { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 5px 0; 5 | } 6 | 7 | .editorForm__label { 8 | margin: 5px 0; 9 | } 10 | 11 | .editorForm__labelText { 12 | display: block; 13 | margin-bottom: 4px; 14 | } 15 | 16 | .editorForm__row { 17 | display: flex; 18 | } 19 | 20 | .editorForm__value { 21 | margin-left: 10px; 22 | } -------------------------------------------------------------------------------- /src/files/file.css: -------------------------------------------------------------------------------- 1 | .file { 2 | padding: 10px 20px; 3 | display: flex; 4 | align-items: baseline; 5 | } 6 | 7 | .file:hover { 8 | background-color: #eee; 9 | } 10 | 11 | .file__name { 12 | flex-grow: 1; 13 | } 14 | 15 | .file__deleteButton { 16 | visibility: hidden; 17 | margin-left: 20px; 18 | } 19 | 20 | .file:hover .file__deleteButton { 21 | visibility: visible; 22 | } 23 | -------------------------------------------------------------------------------- /src/pad/pad-editor.css: -------------------------------------------------------------------------------- 1 | .padEditor__container { 2 | display: flex; 3 | flex-direction: row; 4 | position: relative; 5 | } 6 | 7 | .padEditor__clipEditor { 8 | margin-left: 10px; 9 | } 10 | 11 | .padEdtor__controls { 12 | visibility: hidden; 13 | position: absolute; 14 | left: -111px; 15 | bottom: 0; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | .padEditor__container:hover .padEdtor__controls { 20 | visibility: visible; 21 | } 22 | -------------------------------------------------------------------------------- /src/clip/clip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shouldUpdate } from 'recompose'; 3 | import './clip.css'; 4 | 5 | const Clip = ({ clipId, onClick, selected }) => 6 |
10 |
11 | ; 12 | 13 | export default shouldUpdate( 14 | (props, nextProps) => props.clipId !== nextProps.clipId 15 | || props.selected !== nextProps.selected 16 | )(Clip); 17 | -------------------------------------------------------------------------------- /src/editor.css: -------------------------------------------------------------------------------- 1 | .app__container { 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .app__navigation { 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .app__navigationItem { 12 | padding: 20px 30px; 13 | text-decoration: none; 14 | color: #333; 15 | background-color: #eee; 16 | } 17 | 18 | .app__navigationItem:hover { 19 | background-color: #eaeaea; 20 | } 21 | 22 | .app__navigationItem.m-active { 23 | background-color: #ddd; 24 | } 25 | 26 | .app__content { 27 | margin-left: 10px; 28 | flex-grow: 1; 29 | } 30 | -------------------------------------------------------------------------------- /src/files/file.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withHandlers } from 'recompose'; 3 | import './file.css' 4 | 5 | const File = ({ file, fileId, onButtonClick }) => 6 |
7 | {file.name} 8 | 9 |
10 | ; 11 | 12 | export default withHandlers({ 13 | onButtonClick: ({ onDelete, fileId }) => (event) => { 14 | event.preventDefault(); 15 | onDelete(fileId) 16 | } 17 | })(File); 18 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI 14 | if (!process.env.CI) { 15 | argv.push('--watch'); 16 | } 17 | 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/clip/clip.css: -------------------------------------------------------------------------------- 1 | .clip { 2 | width: 40px; 3 | height: 40px; 4 | background-color: #ddd; 5 | margin-right: 1px; 6 | border: 1px solid transparent; 7 | cursor: pointer; 8 | } 9 | 10 | .clip:last-child { 11 | margin-right: 0 12 | } 13 | 14 | .clip:hover:not(.clip__selected) { 15 | background-color: #cfcfcf; 16 | } 17 | 18 | .clip__notEmpty { 19 | background-color: rgba(186, 218, 84, 0.7); 20 | } 21 | 22 | .clip:hover.clip__notEmpty { 23 | background-color: rgba(186, 218, 84, 0.8) 24 | } 25 | 26 | .clip__on { 27 | background-color: yellow; 28 | } 29 | 30 | .clip__selected { 31 | background-color: rgba(186, 218, 84, 0.9); 32 | border-color: rgba(0, 0, 0, 0.2); 33 | } 34 | -------------------------------------------------------------------------------- /src/data/file-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const FILE_LOADED = 'jsconf2017/file-loader/FILE_LOADED'; 5 | 6 | /** 7 | * -------------------- REDUCER ---------------------------- 8 | */ 9 | export default function fileLoader(state = {}, action) { 10 | switch (action.type) { 11 | case FILE_LOADED: 12 | return { 13 | ...state, 14 | [action.id]: action.file 15 | }; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | /** 22 | * -------------------- ACTION CREATORS ---------------------------- 23 | */ 24 | export const fileLoaded = (id, file) => ({ type: FILE_LOADED, id, file }); 25 | -------------------------------------------------------------------------------- /src/data/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const CHANGE_SETTING = 'jsconf-2017/settings/CHANGE_SETTING'; 5 | 6 | /** 7 | * -------------------- REDUCER ---------------------------- 8 | */ 9 | export default function settings(state = {}, action) { 10 | switch (action.type) { 11 | case CHANGE_SETTING: 12 | return { 13 | ...state, 14 | [action.settingId]: action.value 15 | }; 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | /** 22 | * -------------------- ACTION CREATOR ---------------------------- 23 | */ 24 | export const changeSetting = (settingId, value) => ({ type: CHANGE_SETTING, settingId, value }); 25 | -------------------------------------------------------------------------------- /src/data/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import clips from './clips'; 4 | import controllers from './controllers'; 5 | import fileLoader from './file-loader'; 6 | import files from './files'; 7 | import pads from './pads'; 8 | import scheduler from './scheduler'; 9 | import settings from './settings'; 10 | import tracks from './tracks'; 11 | import videoRenderer from './video-renderer'; 12 | 13 | const appReducer = combineReducers({ 14 | clips, controllers, fileLoader, files, pads, scheduler, settings, tracks, videoRenderer 15 | }); 16 | 17 | const reducer = (state, action) => { 18 | if (action.type === 'init') { 19 | state = action.state; 20 | } 21 | 22 | return appReducer(state, action); 23 | }; 24 | 25 | export default reducer; -------------------------------------------------------------------------------- /src/pad/pads.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux' 3 | import { compose, shouldUpdate } from 'recompose'; 4 | import { createPad } from '../data/pads'; 5 | import PadEditor from './pad-editor'; 6 | 7 | import './pads.css'; 8 | 9 | const Pads = ({ pads, createPad }) => 10 |
11 | {Object.keys(pads).map((padId) => 12 |
13 | 14 |
15 | )} 16 | 17 |
18 | ; 19 | 20 | export default compose( 21 | connect( 22 | (state) => ({ pads: state.pads }), 23 | (dispatch) => ({ createPad() { dispatch(createPad()); }}) 24 | ), 25 | shouldUpdate((props, nextProps) => props.pads !== nextProps.pads) 26 | )(Pads); 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "../node_modules/normalize.css/normalize.css"; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: sans-serif; 7 | color: #333; 8 | } 9 | 10 | button { 11 | padding: 10px; 12 | background: #eee; 13 | color: #333; 14 | font-weight: 400; 15 | border: none; 16 | border-radius: 3px; 17 | cursor: pointer; 18 | outline: 0; 19 | } 20 | 21 | button:hover { 22 | background-color: #e0e0e0; 23 | } 24 | 25 | button.m-no-highlight { 26 | background-color: inherit; 27 | } 28 | 29 | button:disabled, 30 | button:disabled:hover { 31 | cursor: not-allowed; 32 | background-color: #fafafa; 33 | color: #bbb; 34 | } 35 | 36 | input { 37 | outline: 0; 38 | border: 0; 39 | border-bottom: 1px solid #ddd; 40 | line-height: 26px; 41 | } 42 | 43 | input:focus { 44 | border-bottom-color: #ccc; 45 | } 46 | 47 | #root { 48 | background-color: #fff; 49 | height: 100vh; 50 | width: 100vw; 51 | } 52 | -------------------------------------------------------------------------------- /src/projects.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --width: 70vw 3 | } 4 | 5 | .projects { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | height: 100vh; 11 | } 12 | 13 | .projects__list { 14 | list-style: none; 15 | margin: 0; 16 | padding: 0; 17 | width: var(--width); 18 | margin-bottom: 10px; 19 | } 20 | 21 | .projects__listItem { 22 | margin-bottom: 5px; 23 | } 24 | 25 | .projects__link { 26 | padding: 15px; 27 | background: #eee; 28 | display: block; 29 | text-align: center; 30 | text-decoration: none; 31 | transition: background-color 0.2s ease-in-out; 32 | color: #333; 33 | } 34 | 35 | .projects__link:hover { 36 | background: #ddd; 37 | } 38 | 39 | .projects__actions { 40 | display: flex; 41 | justify-content: flex-end; 42 | width: var(--width); 43 | } 44 | 45 | .projects__newForm { 46 | align-self: center; 47 | flex: 1; 48 | } 49 | 50 | .projects__button { 51 | margin-left: 10px; 52 | } 53 | -------------------------------------------------------------------------------- /src/tracks/tracks.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux' 3 | import { createTrack } from '../data/tracks'; 4 | import TrackEditor from './track-editor'; 5 | 6 | class Tracks extends Component { 7 | shouldComponentUpdate(nextProps) { 8 | return nextProps.tracks !== this.props.tracks; 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 | { Object.keys(this.props.tracks).map((trackId) => 15 | 16 | )} 17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | createTrack = () => { 24 | this.props.createTrack(); 25 | } 26 | } 27 | 28 | const mapStateToProps = (state) => ({ 29 | tracks: state.tracks 30 | }); 31 | 32 | const mapDispatchToProps = (dispatch, ownProps) => ({ 33 | createTrack() { 34 | dispatch(createTrack()); 35 | } 36 | }); 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(Tracks); 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 4 | import { Provider } from 'react-redux' 5 | import Projects from './projects'; 6 | import Editor from './editor'; 7 | import Pads from './pad/pads'; 8 | import Settings from './settings/settings'; 9 | import Tracks from './tracks/tracks'; 10 | import FilesList from './files/files-list'; 11 | import store from './lib/store'; 12 | 13 | import './index.css'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ); 29 | -------------------------------------------------------------------------------- /src/data/video-renderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const SET_RENDERPARAM = 'jsconf2017/video-renderer/SET_RENDERPARAM'; 5 | 6 | /** 7 | * -------------------- REDUCER ---------------------------- 8 | */ 9 | const initialState = { 10 | renderParams: { 11 | resolution: "160x90", 12 | backgroundColor: 0x000000, 13 | foregroundColor: 0xe10079, 14 | pointSize: 0.25, 15 | luminanceMin: 0.2, 16 | luminanceMax: 0.9, 17 | r0: 0.9 18 | } 19 | }; 20 | export default function videoRenderer(state = initialState, action) { 21 | switch (action.type) { 22 | case SET_RENDERPARAM: 23 | return { 24 | ...state, 25 | renderParams: { 26 | ...state.renderParams, 27 | ...action.renderParams 28 | } 29 | }; 30 | 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | /** 37 | * -------------------- ACTION CREATORS ---------------------------- 38 | */ 39 | export const setRenderparam = (name, value) => ({ 40 | type: SET_RENDERPARAM, 41 | renderParams: {[name]: value} 42 | }); 43 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | var processEnv = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env[key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'NODE_ENV': JSON.stringify( 17 | process.env.NODE_ENV || 'development' 18 | ), 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': JSON.stringify(publicUrl) 24 | }); 25 | return {'process.env': processEnv}; 26 | } 27 | 28 | module.exports = getClientEnvironment; 29 | -------------------------------------------------------------------------------- /src/live-mode.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import VideoPlayer from './video-player.js'; 4 | import VideoControlsGui from './video-controls-gui.js'; 5 | 6 | import './live-mode.css'; 7 | 8 | class LiveMode extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { visible: false, showVideoControls: false }; 13 | } 14 | 15 | componentDidMount() { 16 | document.addEventListener('keydown', (event) => { 17 | // switch to LIVE mode when pressing TAB 18 | if (event.key === 'Tab') { 19 | event.preventDefault(); 20 | this.setState({ visible: !this.state.visible }); 21 | } 22 | 23 | // show video gui by pressing 'v' 24 | if (event.key === 'v') { 25 | event.preventDefault(); 26 | this.setState({ showVideoControls: !this.state.showVideoControls }); 27 | } 28 | }); 29 | } 30 | 31 | render() { 32 | const { visible, showVideoControls } = this.state; 33 | return ( 34 |
35 | 36 | {visible && showVideoControls && } 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default LiveMode; 43 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | JSConf 2017 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/beatclock.js: -------------------------------------------------------------------------------- 1 | import context from './audio/context'; 2 | import Dilla from 'dilla'; 3 | import EventEmitter from 'event-emitter'; 4 | 5 | class BeatClock { 6 | constructor () { 7 | this.events = new EventEmitter(); 8 | 9 | this.dilla = Dilla(context, { 10 | tempo: 100, 11 | beatsPerBar: 4, 12 | loopLength: 2 13 | }); 14 | 15 | this.dilla.on('tick', this._onTick); 16 | } 17 | 18 | start() { 19 | this.dilla.start(); 20 | } 21 | 22 | stop() { 23 | this.dilla.stop(); 24 | } 25 | 26 | on() { 27 | this.events.on.apply(this.events, arguments); 28 | } 29 | 30 | setBpm(bpm) { 31 | this.dilla.setTempo(bpm); 32 | } 33 | 34 | _onTick = (event) => { 35 | const split = event.position.split('.'); 36 | const tick = parseInt(split[2], 10); 37 | const beat = parseInt(split[1], 10); 38 | const bar = parseInt(split[0], 10); 39 | if (this.lastBar !== bar) { 40 | this.events.emit('bar', bar); 41 | } 42 | this.lastBar = bar; 43 | 44 | if (this.lastBeat !== beat) { 45 | this.events.emit('beat', tick, beat, bar); 46 | } 47 | this.lastBeat = beat; 48 | 49 | if (this.lastTick !== tick) { 50 | this.events.emit('tick', tick, beat, bar); 51 | } 52 | this.lastTick = tick; 53 | }; 54 | } 55 | 56 | export default BeatClock; 57 | -------------------------------------------------------------------------------- /src/clip/play-audio-clip.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import context from '../lib/audio/context'; 3 | 4 | export default class PlayAudioClip extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { playing: false }; 10 | } 11 | 12 | shouldComponentUpdate() { 13 | return false; 14 | } 15 | 16 | render() { 17 | const { playing } = this.state; 18 | return ; 19 | } 20 | 21 | playPause = (event) => { 22 | event.preventDefault(); 23 | this.state.playing ? this.pause() : this.play(); 24 | } 25 | 26 | play = () => { 27 | const { buffer, config } = this.props; 28 | const audioNode = context.createBufferSource(); 29 | const gainNode = context.createGain(); 30 | audioNode.buffer = buffer; 31 | gainNode.gain.value = config.gain; 32 | audioNode.connect(gainNode); 33 | gainNode.connect(context.destination); 34 | audioNode.onended = this.pause; 35 | audioNode.start(); 36 | 37 | this.setState({ 38 | playing: true, 39 | audioNode 40 | }); 41 | } 42 | 43 | pause = () => { 44 | const { audioNode } = this.state; 45 | if (audioNode) { 46 | audioNode.disconnect(); 47 | audioNode.stop(); 48 | } 49 | 50 | this.setState({ 51 | audioNode: null, 52 | playing: false 53 | }); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/data/controllers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const ADD_CONTROLLER = 'jsconf2017/controllers/ADD_CONTROLLER'; 5 | const MAP_CONTROLLER_TO_PAD = 'jsconf2017/controllers/MAP_CONTROLLER_TO_PAD'; 6 | const REMOVE_CONTROLLER = 'jsconf2017/controllers/REMOVE_CONTROLLER'; 7 | 8 | /** 9 | * -------------------- REDUCER ---------------------------- 10 | */ 11 | export default function controllers(state = {}, action) { 12 | const { id, pad } = action; 13 | switch (action.type) { 14 | case ADD_CONTROLLER: 15 | return { 16 | ...state, 17 | [id]: { 18 | id, 19 | pad, 20 | controller: action.controller, 21 | } 22 | }; 23 | case MAP_CONTROLLER_TO_PAD: 24 | return { 25 | ...state, 26 | [id]: { 27 | ...state[id], 28 | pad 29 | } 30 | }; 31 | case REMOVE_CONTROLLER: 32 | const copy = { ...state }; 33 | delete copy[id]; 34 | return copy; 35 | default: 36 | return state; 37 | } 38 | } 39 | 40 | /** 41 | * -------------------- ACTION CREATORS ---------------------------- 42 | */ 43 | export const addController = (id, controller, pad) => ({ type: ADD_CONTROLLER, id, controller, pad }); 44 | export const mapControllerToPad = (id, pad) => ({ type: MAP_CONTROLLER_TO_PAD, id, pad }); 45 | export const removeController = (id) => ({ type: REMOVE_CONTROLLER, id }); 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSConf.eu opening performance 2017 2 | 3 | ## Setup 4 | 5 | 1. Install yarn: 6 | 1. Run: `yarn` 7 | 8 | ## Development 9 | 10 | `yarn start` 11 | 12 | ## Build a new version of the app 13 | 14 | 1. Run `yarn run build-electron` (it might ask you to install all sorts of deps for Windows) 15 | 16 | ## The Talks We Used 17 | 18 | Ashley Williams - A brief history and mishistory of modularity - Nordic.js 2016 19 | https://www.youtube.com/watch?v=LfOVyNQK5io 20 | 21 | Anjana Vakil: Learning Functional Programming with JavaScript - JSUnconf 2016 22 | https://www.youtube.com/watch?v=e-5obm1G_FY 23 | 24 | Jake Archibald - Show Them What You Got 25 | https://vimeo.com/album/3953264/video/165995029 26 | 27 | André Staltz (@andrestaltz) - You will learn RxJS at ng-europe 2016 28 | https://www.youtube.com/watch?v=uQ1zhJHclvs 29 | 30 | Philip Roberts - JSConf.eu 2014 - What the heck is the event loop anyway? 31 | https://www.youtube.com/watch?v=8aGhZQkoFbQ 32 | 33 | PolyConf 16: The Seif Project / Douglas Crockford 34 | https://www.youtube.com/watch?v=O9AwYiwIvXE 35 | 36 | ReactiveConf 2016 - David Nolen - Through the Looking Glass 37 | https://www.youtube.com/watch?v=lkh4hjyHdWA 38 | 39 | The State of Javascript - Jack Franklin _ August 2016 40 | https://www.youtube.com/watch?v=5NIL3Epadj0 41 | 42 | Raquel Vélez - Wombat-Driven Understanding - An Interactive Guide To Using npm - JSConf.Asia 2016 43 | https://www.youtube.com/watch?v=7MbXsRS-ZLg 44 | -------------------------------------------------------------------------------- /src/lib/video/dot-matrix-shader.js: -------------------------------------------------------------------------------- 1 | import {Color} from 'three'; 2 | 3 | //language=GLSL 4 | const vertexShader = ` 5 | uniform float size; 6 | uniform float scale; 7 | uniform sampler2D texture; 8 | uniform float lumMin; 9 | uniform float lumMax; 10 | 11 | #include 12 | 13 | const vec3 lumaComp = vec3(0.299, 0.587, 0.114); 14 | 15 | void main() { 16 | vec3 color = texture2D(texture, uv).rgb; 17 | // dot product used to get the sum of components 18 | float luma = dot(lumaComp * color, vec3(1.0)); 19 | 20 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 21 | float distanceScaling = (scale / -mvPosition.z); 22 | 23 | gl_Position = projectionMatrix * mvPosition; 24 | gl_PointSize = smoothstep(lumMin, lumMax, luma) * size * distanceScaling; 25 | } 26 | `; 27 | 28 | //language=GLSL 29 | const fragmentShader = ` 30 | uniform vec3 pointColor; 31 | uniform float r0; 32 | 33 | float circle(vec2 p) { 34 | return 1.0 - smoothstep(r0, 1.0, length(p)); 35 | } 36 | 37 | void main() { 38 | vec2 p = 2.0 * gl_PointCoord - vec2(1.0, 1.0); 39 | gl_FragColor = vec4(pointColor, circle(p)); 40 | } 41 | `; 42 | 43 | export default { 44 | vertexShader, fragmentShader, 45 | transparent: true, 46 | alphaTest: 0.5, 47 | depthWrite: false, 48 | uniforms: { 49 | size: { value: 0.25 }, 50 | scale: { value: 0 }, 51 | texture: { value: null }, 52 | pointColor: {type: 'c', value: new Color()}, 53 | lumMin: {value: 0.0}, 54 | lumMax: {value: 0.0}, 55 | r0: {value: 0.0} 56 | } 57 | } -------------------------------------------------------------------------------- /src/lib/audio-graph.js: -------------------------------------------------------------------------------- 1 | import context from './audio/context'; 2 | import _ from 'lodash'; 3 | 4 | export default { 5 | tracks: {}, 6 | filters: {}, 7 | storeObject: null, 8 | 9 | init(storeObject) { 10 | this.storeObject = storeObject; 11 | this.removeTrack = this.removeTrack.bind(this); 12 | 13 | this.previousTracks = {}; 14 | storeObject.subscribe(() => { 15 | const { tracks } = this.storeObject.getState(); 16 | if (this.previousTracks !== tracks) { 17 | this.synchronizeGraph(); 18 | this.previousTracks = tracks; 19 | } 20 | }); 21 | 22 | this.synchronizeGraph(); 23 | }, 24 | 25 | synchronizeGraph() { 26 | const { tracks } = this.storeObject.getState(); 27 | const currentTrackIds = Object.keys(tracks); 28 | const previousTrackIds = Object.keys(this.previousTracks); 29 | 30 | if (previousTrackIds.length > currentTrackIds.length) { 31 | _.difference(previousTrackIds, currentTrackIds).forEach(this.removeTrack) 32 | } 33 | 34 | currentTrackIds.forEach((trackId) => { 35 | const trackConfig = tracks[trackId]; 36 | let track = this.tracks[trackId]; 37 | if (!track) { 38 | track = context.createGain(); 39 | track.connect(context.destination); 40 | this.tracks[trackId] = track; 41 | } 42 | track.gain.value = parseFloat(trackConfig.gain, 10); 43 | }); 44 | }, 45 | 46 | getTracks() { 47 | return this.tracks; 48 | }, 49 | 50 | removeTrack(trackId) { 51 | this.tracks[trackId].disconnect(); 52 | delete this.tracks[trackId]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pad/pad.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux' 3 | import uuid from 'uuid'; 4 | import Clip from '../clip/clip'; 5 | import { createClip } from '../data/clips'; 6 | import { selectClip } from '../data/pads'; 7 | import './pad.css'; 8 | 9 | class Pad extends Component { 10 | shouldComponentUpdate(nextProps) { 11 | return this.props.pad !== nextProps.pad 12 | || this.props.selectedClipId !== nextProps.selectedClipId; 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | { [...Array(8)].map((_, y) => 19 |
20 | { [...Array(8)].map((_, x) => this.renderClip(x, y)) } 21 |
22 | )} 23 |
24 | ); 25 | } 26 | 27 | renderClip(x, y) { 28 | const { pad, onClipSelected, selectedClipId } = this.props; 29 | const row = pad.clips[y]; 30 | const clipId = row ? row[x] : undefined; 31 | const isEmpty = !clipId; 32 | const key = `pad-clip-${x}-${y}`; 33 | if (isEmpty) { return this.props.createClip(x, y)}/>; } 34 | return onClipSelected(clipId)} 39 | /> 40 | } 41 | } 42 | 43 | const mapDispatchToProps = (dispatch, { pad }) => ({ 44 | createClip(x, y) { 45 | const id = uuid.v4(); 46 | dispatch(createClip(x, y, id, pad.id)); 47 | dispatch(selectClip(id, pad.id)); 48 | } 49 | }); 50 | 51 | export default connect(null, mapDispatchToProps)(Pad); 52 | -------------------------------------------------------------------------------- /scripts/build-electron.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const packager = require('electron-packager'); 3 | const path = require('path'); 4 | 5 | const dirPath = path.join(__dirname, '..', 'build'); 6 | const outPath = path.join(__dirname, '..', 'electron-build'); 7 | 8 | const buildPackageJSONPath = path.join(__dirname, '..', 'build', 'package.json'); 9 | 10 | // only add the dependencies for the electron app here that are not bundled by webpack. 11 | fs.writeFileSync(buildPackageJSONPath, JSON.stringify({ 12 | name: 'jsconf-2017', 13 | main: 'start-electron.js', 14 | dependencies: { 15 | "appdirectory": "0.1.0", 16 | 'electron-log': '1.2.2', 17 | 'express': '4.14.0', 18 | 'portfinder': '1.0.10' 19 | } 20 | }, null, 2)); 21 | 22 | const startFilePath = path.join(__dirname, 'start-electron.js'); 23 | const startFile = fs.readFileSync(startFilePath).toString(); 24 | const startFileDestPath = path.join(__dirname, '..', 'build', 'start-electron.js'); 25 | fs.writeFileSync(startFileDestPath, startFile); 26 | 27 | const prodEnvFilePath = path.join(__dirname, '..', 'build', 'prod.json'); 28 | fs.writeFileSync(prodEnvFilePath, JSON.stringify({})); 29 | 30 | require('child_process').exec('cd build && npm install', (err, a, b) => { 31 | packager({ 32 | dir: dirPath, 33 | arch: 'x64', 34 | platform: ['darwin', 'win32'], 35 | prune: true, 36 | overwrite: true, 37 | out: outPath 38 | }, (err, appPaths) => { 39 | if (err) { 40 | console.log('Could not create electron build', err.toString()); 41 | } else { 42 | console.log('Successfully built the electron app'); 43 | appPaths.forEach((p) => console.log(p)); 44 | } 45 | }); 46 | 47 | }) 48 | 49 | -------------------------------------------------------------------------------- /src/files/drag-and-drop-receiver.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { isAudio, isVideo } from '../lib/regular-expressions'; 3 | 4 | export default class DragAndDropReveicer extends Component { 5 | componentDidMount() { 6 | const { body } = document; 7 | body.addEventListener('dragenter', this.onDragEnter); 8 | body.addEventListener('dragover', this.onDragOver); 9 | body.addEventListener('drop', this.onDrop); 10 | body.addEventListener('dragleave', this.onDragLeave); 11 | } 12 | 13 | componentWillUnmount() { 14 | const { body } = document; 15 | body.removeEventListener('dragenter', this.onDragEnter); 16 | body.removeEventListener('dragover', this.onDragOver); 17 | body.removeEventListener('drop', this.onDrop); 18 | body.removeEventListener('dragleave', this.onDragLeave); 19 | } 20 | 21 | onDragOver = (event) => { 22 | event.preventDefault(); 23 | event.stopPropagation(); 24 | return false; 25 | } 26 | 27 | onDragLeave = (event) => { 28 | event.preventDefault(); 29 | event.stopPropagation(); 30 | return false; 31 | } 32 | 33 | onDragEnter = (event) => { 34 | event.preventDefault(); 35 | event.stopPropagation(); 36 | return false; 37 | } 38 | 39 | onDrop = (event) => { 40 | event.preventDefault(); 41 | event.stopPropagation(); 42 | const { dataTransfer } = event; 43 | if (dataTransfer) { 44 | const { files } = dataTransfer; 45 | 46 | for (var i = 0; i < files.length; i++) { 47 | const file = files[i]; 48 | if (isAudio.test(file.type) || isVideo.test(file.type)) { 49 | this.props.onDrop(file.path); 50 | } 51 | } 52 | } 53 | return false; 54 | } 55 | 56 | render() { 57 | return
; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/data/files.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { isAudio, isVideo } from '../lib/regular-expressions'; 3 | 4 | /** 5 | * -------------------- ACTION TYPES ---------------------------- 6 | */ 7 | export const ADD_FILE = 'jsconf2017/files/ADD_FILE'; 8 | export const REMOVE_FILE = 'jsconf2017/files/REMOVE_FILE'; 9 | 10 | /** 11 | * -------------------- REDUCER ---------------------------- 12 | */ 13 | export default function(state = {}, action) { 14 | const { id, file } = action; 15 | switch (action.type) { 16 | case ADD_FILE: 17 | return { 18 | ...state, 19 | [id]: file 20 | } 21 | case REMOVE_FILE: 22 | const files = { ...state }; 23 | delete files[id]; 24 | return files; 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | /** 31 | * -------------------- ACTION CREATORS ---------------------------- 32 | */ 33 | export const addFile = (id, file) => ({ type: ADD_FILE, id, file }); 34 | export const removeFile = (id) => ({ type: REMOVE_FILE, id }); 35 | 36 | /** 37 | * -------------------- SELECTORS ---------------------------- 38 | */ 39 | const getFiles = (state) => state.files 40 | 41 | function fileIsAudio(file) { return isAudio.test(file.location); } 42 | function fileIsVideo(file) { return isVideo.test(file.location); } 43 | 44 | export const getAudioFiles = createSelector( 45 | [getFiles], 46 | (files) => Object.keys(files) 47 | .filter((id) => fileIsAudio(files[id])) 48 | .reduce((res, id) => Object.assign(res, { [id]: files[id] }), {}) 49 | ); 50 | 51 | export const getVideoFiles = createSelector( 52 | [getFiles], 53 | (files) => Object.keys(files) 54 | .filter((id) => fileIsVideo(files[id])) 55 | .reduce((res, id) => Object.assign(res, { [id]: files[id] }), {}) 56 | ) 57 | -------------------------------------------------------------------------------- /src/settings/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import { connect } from 'react-redux' 4 | import { changeSetting } from '../data/settings'; 5 | import { saveProjectAsZip } from '../lib/files'; 6 | const { dialog } = require('electron').remote; 7 | import '../styles/forms.css'; 8 | import './settings.css'; 9 | 10 | class Settings extends React.Component { 11 | render() { 12 | const { settings } = this.props; 13 | 14 | if (_.isEmpty(settings)) { return null; } 15 | 16 | return ( 17 |
18 | 25 | 26 | 29 | 30 | 33 |
34 | ); 35 | } 36 | 37 | onBpmChanged = (event) => this.props.changeSetting('bpm', parseInt(event.target.value, 10)); 38 | 39 | exportProject = (event) => { 40 | event.preventDefault(); 41 | const { params: { project_id } } = this.props; 42 | saveProjectAsZip(project_id); 43 | }; 44 | 45 | closeProject = () => { 46 | const result = dialog.showMessageBox({ 47 | message: 'Are you shure you want to close the project?', 48 | title: 'Closing project', 49 | buttons: ['No', 'Yes'] 50 | }); 51 | 52 | if (result === 1) { 53 | location.href = '/' 54 | } 55 | } 56 | } 57 | 58 | 59 | const mapStateToProps = (state) => ({ settings: state.settings }); 60 | const mapDispatchToProps = (dispatch) => ({ 61 | changeSetting(settingId, value){ dispatch(changeSetting(settingId, value)) } 62 | }); 63 | 64 | export default connect(mapStateToProps, mapDispatchToProps)(Settings); 65 | -------------------------------------------------------------------------------- /src/pad/pad-editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux' 3 | const { dialog } = require('electron').remote; 4 | import Pad from './pad'; 5 | import ClipEditor from '../clip/clip-editor'; 6 | import { selectClip, removePad } from '../data/pads'; 7 | 8 | import './pad-editor.css'; 9 | 10 | class PadEditor extends Component { 11 | shouldComponentUpdate(nextProps) { 12 | return this.props.pad !== nextProps.pad 13 | || this.props.clips !== nextProps.clips; 14 | } 15 | 16 | render() { 17 | const { pad, padId, clips, removePad } = this.props; 18 | const selectedClip = clips[pad.selectedClipId]; 19 | const hasClipSelected = !!selectedClip; 20 | return ( 21 |
22 |
23 | 24 |
25 | 31 |
32 | { hasClipSelected && ( 33 | 34 | )} 35 |
36 |
37 | ); 38 | } 39 | 40 | onClipSelected = (selectedClipId) => { 41 | const isTheSame = selectedClipId === this.props.pad.selectedClipId; 42 | 43 | this.props.selectClip(isTheSame ? null : selectedClipId); 44 | } 45 | } 46 | 47 | const mapStateToProps = (state, { padId }) => { 48 | return { 49 | pad: state.pads[padId], 50 | clips: state.clips 51 | }; 52 | }; 53 | 54 | const mapDispatchToProps = (dispatch, { padId }) => ({ 55 | selectClip(selectedClipId) { 56 | dispatch(selectClip(selectedClipId, padId)); 57 | }, 58 | removePad() { 59 | const result = dialog.showMessageBox({ 60 | message: 'Are you sure you want to remove this pad?', 61 | title: 'Removing Pad', 62 | buttons: ['No', 'Yes'] 63 | }); 64 | if (result === 1) { 65 | dispatch(removePad(padId)); 66 | } 67 | } 68 | }); 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(PadEditor); 71 | -------------------------------------------------------------------------------- /src/data/tracks.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | 3 | /** 4 | * -------------------- ACTION TYPES ---------------------------- 5 | */ 6 | const CREATE_TRACK = 'jsconf2017/tracks/CREATE_TRACK'; 7 | const CHANGE_TRACK_NAME = 'jsconf2017/tracks/CHANGE_TRACK_NAME'; 8 | const CHANGE_TRACK_GAIN = 'jsconf2017/tracks/CHANGE_TRACK_GAIN'; 9 | const ADD_FILTER = 'jsconf2017/tracks/ADD_FILTER'; 10 | const REMOVE_TRACK = 'jsconf2017/tracks/REMOVE_TRACK'; 11 | 12 | /** 13 | * -------------------- REDUCER ---------------------------- 14 | */ 15 | export default function tracks(state = {}, action) { 16 | const { id } = action; 17 | const track = state[id]; 18 | 19 | switch (action.type) { 20 | case CREATE_TRACK: 21 | const trackId = uuid.v4(); 22 | const trackCount = Object.keys(state).length + 1; 23 | const trackName = `track${trackCount}`; 24 | return { 25 | ...state, 26 | [trackId]: { name: trackName, gain: 1, filters: [], id: trackId } 27 | }; 28 | case CHANGE_TRACK_NAME: 29 | const { name } = action; 30 | return { 31 | ...state, 32 | [id]: { 33 | ...track, 34 | name 35 | } 36 | } 37 | case CHANGE_TRACK_GAIN: 38 | const { gain } = action; 39 | return { 40 | ...state, 41 | [id]: { 42 | ...track, 43 | gain 44 | } 45 | } 46 | case ADD_FILTER: 47 | const { filter } = action; 48 | return { 49 | ...state, 50 | [id]: { 51 | ...track, 52 | filters: [...track.filters, filter] 53 | } 54 | }; 55 | case REMOVE_TRACK: 56 | if (id === 'master') { return state; } 57 | const copy = { ...state }; 58 | delete copy[id]; 59 | return copy; 60 | default: 61 | return state; 62 | } 63 | } 64 | 65 | /** 66 | * -------------------- ACTION CREATORS ---------------------------- 67 | */ 68 | export const createTrack = () => ({ type: CREATE_TRACK }); 69 | export const changeTrackName = (id, name) => ({ type: CHANGE_TRACK_NAME, id, name }); 70 | export const changeTrackGain = (id, gain) => ({ type: CHANGE_TRACK_GAIN, id, gain }); 71 | export const removeTrack = (id) => ({ type: REMOVE_TRACK, id }); 72 | -------------------------------------------------------------------------------- /src/data/blank_project.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "gain": 1, 4 | "bpm": 100 5 | }, 6 | "tracks": { 7 | "master": { 8 | "name": "master", 9 | "id": "master", 10 | "gain": 1, 11 | "filters": [ 12 | ] 13 | } 14 | }, 15 | "filters": { 16 | }, 17 | "pads": { 18 | "pad1": { 19 | "id": "pad1", 20 | "clips": [ 21 | [ 22 | null, 23 | null, 24 | null, 25 | null, 26 | null, 27 | null, 28 | null, 29 | null 30 | ], 31 | [ 32 | null, 33 | null, 34 | null, 35 | null, 36 | null, 37 | null, 38 | null, 39 | null 40 | ], 41 | [ 42 | null, 43 | null, 44 | null, 45 | null, 46 | null, 47 | null, 48 | null, 49 | null 50 | ], 51 | [ 52 | null, 53 | null, 54 | null, 55 | null, 56 | null, 57 | null, 58 | null, 59 | null 60 | ], 61 | [ 62 | null, 63 | null, 64 | null, 65 | null, 66 | null, 67 | null, 68 | null, 69 | null 70 | ], 71 | [ 72 | null, 73 | null, 74 | null, 75 | null, 76 | null, 77 | null, 78 | null, 79 | null 80 | ], 81 | [ 82 | null, 83 | null, 84 | null, 85 | null, 86 | null, 87 | null, 88 | null, 89 | null 90 | ], 91 | [ 92 | null, 93 | null, 94 | null, 95 | null, 96 | null, 97 | null, 98 | null, 99 | null 100 | ] 101 | ], 102 | "selectedClipId": null 103 | } 104 | }, 105 | "clips": { 106 | }, 107 | "files": { 108 | }, 109 | "scheduler": { 110 | "scheduled": {}, 111 | "playing": {}, 112 | "toStop": {} 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/files/files-list.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import { compose, shouldUpdate } from 'recompose'; 4 | import { removeFile } from '../data/files'; 5 | import { deleteFile } from '../lib/files'; 6 | import store from '../lib/store'; 7 | import File from './file'; 8 | const { dialog } = require('electron').remote; 9 | 10 | import './files-list.css'; 11 | 12 | const FilesList = ({ files, deleteFile }) => 13 |
14 | { Object.keys(files).map((fileId) => 15 | 21 | )} 22 |
23 | ; 24 | 25 | const mapStateToProps = (state) => ({ 26 | files: state.files, 27 | clips: state.clips 28 | }); 29 | 30 | const mapDispatchToProps = (dispatch, props) => ({ 31 | deleteFile(fileId) { 32 | const { params: { project_id } } = props; 33 | const { clips, files } = store.getState(); 34 | const affectedClips = Object.keys(clips) 35 | .map((clipId) => 36 | clips[clipId].file === fileId 37 | ) 38 | .filter(Boolean) 39 | const affectedAmount = affectedClips.length; 40 | const clipOrClips = affectedAmount === 1 ? 'clip' : 'clips'; 41 | const extraMessage = `This file is used in ${affectedAmount} ${clipOrClips}, cannot remove it`; 42 | const message = 'Are you shure you want to remove the file?'; 43 | 44 | if (affectedAmount) { 45 | dialog.showMessageBox({ 46 | message: extraMessage, 47 | title: 'Removing file', 48 | buttons: ['Okay'] 49 | }); 50 | } else { 51 | const result = dialog.showMessageBox({ 52 | message, 53 | title: 'Removing file', 54 | buttons: ['No', 'Yes'] 55 | }); 56 | if (result === 1) { 57 | deleteFile(project_id, files[fileId].location); 58 | dispatch(removeFile(fileId)); 59 | } 60 | } 61 | } 62 | }); 63 | 64 | export default compose( 65 | connect(mapStateToProps, mapDispatchToProps), 66 | shouldUpdate((props, nextProps) => props.files !== nextProps.files) 67 | )(FilesList); 68 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | var nodePaths = (process.env.NODE_PATH || '') 23 | .split(process.platform === 'win32' ? ';' : ':') 24 | .filter(Boolean) 25 | .map(resolveApp); 26 | 27 | // config after eject: we're in ./config/ 28 | module.exports = { 29 | appBuild: resolveApp('build'), 30 | appPublic: resolveApp('public'), 31 | appHtml: resolveApp('public/index.html'), 32 | appIndexJs: resolveApp('src/index.js'), 33 | appPackageJson: resolveApp('package.json'), 34 | appSrc: resolveApp('src'), 35 | testsSetup: resolveApp('src/setupTests.js'), 36 | appNodeModules: resolveApp('node_modules'), 37 | ownNodeModules: resolveApp('node_modules'), 38 | nodePaths: nodePaths 39 | }; 40 | 41 | 42 | 43 | // config before publish: we're in ./packages/react-scripts/config/ 44 | if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1) { 45 | module.exports = { 46 | appBuild: resolveOwn('../../../build'), 47 | appPublic: resolveOwn('../template/public'), 48 | appHtml: resolveOwn('../template/public/index.html'), 49 | appIndexJs: resolveOwn('../template/src/index.js'), 50 | appPackageJson: resolveOwn('../package.json'), 51 | appSrc: resolveOwn('../template/src'), 52 | testsSetup: resolveOwn('../template/src/setupTests.js'), 53 | appNodeModules: resolveOwn('../node_modules'), 54 | ownNodeModules: resolveOwn('../node_modules'), 55 | nodePaths: nodePaths 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/video-controls-gui.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | 4 | import {GUI} from 'dat.gui/build/dat.gui.js'; 5 | import {setRenderparam} from './data/video-renderer'; 6 | 7 | import './video-controls-gui.css'; 8 | 9 | class VideoControlsGui extends Component { 10 | componentWillUpdate(nextProps) { 11 | if (!this.gui) { return; } 12 | 13 | const {renderParams} = nextProps; 14 | Object.keys(renderParams).forEach(name => { 15 | if (renderParams[name] !== this.guiState[name]) { 16 | this.guiState[name] = renderParams[name]; 17 | this.guiControllers[name].updateDisplay(); 18 | } 19 | }); 20 | } 21 | 22 | componentDidMount() { 23 | this.gui = new GUI({autoPlace: false}); 24 | this.guiState = Object.assign({}, this.props.renderParams); 25 | 26 | this.guiControllers = { 27 | backgroundColor: this.gui.addColor(this.guiState, 'backgroundColor'), 28 | foregroundColor: this.gui.addColor(this.guiState, 'foregroundColor'), 29 | pointSize: this.gui.add(this.guiState, 'pointSize', 0, .5), 30 | luminanceMin: this.gui.add(this.guiState, 'luminanceMin', 0, 1), 31 | luminanceMax: this.gui.add(this.guiState, 'luminanceMax', 0, 1), 32 | r0: this.gui.add(this.guiState, 'r0', 0, 1), 33 | resolution: this.gui.add(this.guiState, 'resolution', [ 34 | '80x45', '96x54', '160x90', '240x135', '320x180', '640x360' 35 | ]) 36 | }; 37 | 38 | Object.keys(this.guiControllers).forEach(name => { 39 | this.guiControllers[name].onChange(value => { 40 | this.props.onValueChange(name, value); 41 | }); 42 | }); 43 | 44 | this.container.appendChild(this.gui.domElement); 45 | } 46 | 47 | componentWillUnmount() { 48 | this.gui.domElement.parentNode.removeChild(this.gui.domElement); 49 | this.gui.destroy(); 50 | this.gui = null; 51 | this.guiControllers = {}; 52 | this.guiState = {}; 53 | } 54 | 55 | render() { 56 | return ( 57 |
this.container = el} /> 58 | ); 59 | } 60 | } 61 | 62 | const mapStateToProps = (state) => ({ 63 | renderParams: state.videoRenderer.renderParams 64 | }); 65 | 66 | const mapDispatchToProps = (dispatch) => ({ 67 | onValueChange: (name, value) => dispatch(setRenderparam(name, value)) 68 | }); 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)(VideoControlsGui) -------------------------------------------------------------------------------- /src/video-player.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import VideoRenderer from './lib/video/renderer'; 4 | 5 | import './video-player.css'; 6 | 7 | class VideoPlayer extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.videoRenderer = new VideoRenderer(); 12 | this.simpleVideoContainer = document.createElement('div'); 13 | this.simpleVideoContainer.className = 'simpleVideoContainer'; 14 | } 15 | 16 | render() { 17 | return ( 18 |
this.container = el } className="videoPlayer" /> 19 | ); 20 | } 21 | 22 | componentDidMount() { 23 | this.container.appendChild(this.simpleVideoContainer); 24 | this.container.appendChild(this.videoRenderer.getDomElement()); 25 | } 26 | 27 | componentWillUpdate({ renderParams, videos, singleVideo }) { 28 | this.videoRenderer.setRenderParams(renderParams); 29 | this.videoRenderer.setVideos(videos); 30 | 31 | if (singleVideo && singleVideo !== this.props.singleVideo) { 32 | this.simpleVideoContainer.innerHTML = ''; 33 | this.simpleVideoContainer.appendChild(singleVideo); 34 | } 35 | 36 | if (!singleVideo) { 37 | this.simpleVideoContainer.innerHTML = ''; 38 | } 39 | } 40 | 41 | componentWillUnmount() { 42 | this.videoRenderer.stop(); 43 | const domEl = this.videoRenderer.getDomElement(); 44 | domEl.parentNode.removeChild(domEl); 45 | } 46 | } 47 | 48 | const mapStateToProps = (state, ownProps) => { 49 | const { scheduler, clips, videoRenderer } = state; 50 | const playingFileIds = Object.keys(scheduler.playing); 51 | 52 | const videos = playingFileIds 53 | .map((fileId) => { 54 | const clip = clips[scheduler.playing[fileId].clipId]; 55 | if (clip && !clip.noFilter) { 56 | return scheduler.playing[fileId].payload.videoElement 57 | } 58 | return undefined; 59 | }) 60 | .filter(Boolean); 61 | 62 | const singleVideo = playingFileIds 63 | .map((fileId) => { 64 | const clip = clips[scheduler.playing[fileId].clipId]; 65 | if (clip && clip.noFilter) { 66 | return scheduler.playing[fileId].payload.videoElement 67 | } 68 | return undefined; 69 | }) 70 | .filter(Boolean) 71 | .pop(); 72 | 73 | const renderParams = videoRenderer.renderParams; 74 | 75 | return { videos, singleVideo, renderParams: { ...ownProps, ...renderParams } }; 76 | }; 77 | 78 | export default connect(mapStateToProps, null)(VideoPlayer); -------------------------------------------------------------------------------- /src/data/scheduler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const ADD_SCHEDULED = 'jsconf2017/scheduler/ADD_SCHEDULED'; 5 | const ADD_PLAYING = 'jsconf2017/scheduler/ADD_PLAYING'; 6 | const ADD_TOSTOP = 'jsconf2017/scheduler/ADD_TOSTOP'; 7 | const MEDIA_ENDED = 'jsconf2017/scheduler/MEDIA_ENDED'; 8 | const FLUSH_SCHEDULED = 'jsconf2017/scheduler/FLUSH_SCHEDULED'; 9 | 10 | /** 11 | * -------------------- REDUCER ---------------------------- 12 | */ 13 | export default function scheduler(state, action) { 14 | if (!state) { 15 | state = { scheduled: {}, toStop: {}, playing: {} }; 16 | } 17 | 18 | const { id } = action; 19 | 20 | switch (action.type) { 21 | case ADD_SCHEDULED: 22 | return { 23 | ...state, 24 | scheduled: { 25 | ...state.scheduled, 26 | [id]: true 27 | } 28 | }; 29 | case FLUSH_SCHEDULED: 30 | return { 31 | ...state, 32 | scheduled: {}, 33 | toStop: {} 34 | }; 35 | case ADD_PLAYING: 36 | const { payload, clipId } = action; 37 | return { 38 | ...state, 39 | playing: { 40 | ...state.playing, 41 | [id]: { payload, clipId } 42 | } 43 | }; 44 | case ADD_TOSTOP: 45 | return { 46 | ...state, 47 | toStop: { 48 | ...state.toStop, 49 | [id]: true 50 | } 51 | }; 52 | case MEDIA_ENDED: 53 | const playingCopy = { ...state.playing }; 54 | 55 | delete playingCopy[id]; 56 | 57 | return { 58 | ...state, 59 | playing: playingCopy 60 | }; 61 | default: 62 | return state; 63 | } 64 | } 65 | 66 | /** 67 | * -------------------- HELPERS ---------------------------- 68 | */ 69 | export const playingId = (clipId, fileId) => `file:${fileId}-clip${clipId}`; 70 | export const isPlaying = (playingState, clip) => playingState[playingId(clip.id, clip.file)] || playingState[playingId(clip.id, clip.videoFile)] 71 | 72 | /** 73 | * -------------------- ACTION CREATORS ---------------------------- 74 | */ 75 | export const addPlaying = (fileId, payload, clipId) => ({ type: ADD_PLAYING, id: playingId(clipId, fileId), payload, clipId}); 76 | export const addScheduled = (id) => ({ type: ADD_SCHEDULED, id }); 77 | export const mediaEnded = (fileId, clipId) => ({ type: MEDIA_ENDED, id: playingId(clipId, fileId) }); 78 | export const flushScheduled = () => ({ type: FLUSH_SCHEDULED }); 79 | export const scheduleStop = (id) => ({ type: ADD_TOSTOP, id }); 80 | -------------------------------------------------------------------------------- /src/lib/junk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied over from https://github.com/sindresorhus/junk/blob/master/index.js 3 | * because the uglifyjs we're does not understand arrow functions. 4 | * Their licence applies: 5 | * 6 | The MIT License (MIT) 7 | 8 | Copyright (c) Sindre Sorhus (sindresorhus.com) 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | 28 | */ 29 | 30 | // # All 31 | // /^npm-debug\.log$/, // npm error log 32 | // /^\..*\.swp$/, // vim state 33 | // // macOS 34 | // /^\.DS_Store$/, // stores custom folder attributes 35 | // /^\.AppleDouble$/, // stores additional file resources 36 | // /^\.LSOverride$/, // contains the absolute path to the app to be used 37 | // /^Icon\r$/, // custom Finder icon: http://superuser.com/questions/298785/icon-file-on-os-x-desktop 38 | // /^\._.*/, // thumbnail 39 | // /^\.Spotlight-V100(?:$|\/)/, // directory that might appear on external disk 40 | // /\.Trashes/, // file that might appear on external disk 41 | // /^__MACOSX$/, // resource fork 42 | // # Linux 43 | // /~$/, // backup file 44 | // # Windows 45 | // /^Thumbs\.db$/, // image file cache 46 | // /^ehthumbs\.db$/, // folder config file 47 | // /^Desktop\.ini$/ // stores custom folder attributes 48 | 49 | exports.re = /^npm-debug\.log$|^\..*\.swp$|^\.DS_Store$|^\.AppleDouble$|^\.LSOverride$|^Icon\r$|^\._.*|^\.Spotlight-V100(?:$|\/)|\.Trashes|^__MACOSX$|~$|^Thumbs\.db$|^ehthumbs\.db$|^Desktop\.ini$/; 50 | 51 | exports.is = filename => exports.re.test(filename); 52 | 53 | exports.not = filename => !exports.is(filename); 54 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, IndexLink } from 'react-router'; 3 | import { connect } from 'react-redux' 4 | import LiveMode from './live-mode'; 5 | import DragAndDropReveicer from './files/drag-and-drop-receiver'; 6 | import Loader from './lib/loader'; 7 | import uuid from 'uuid'; 8 | import './editor.css'; 9 | 10 | import { addFile } from './data/files'; 11 | import { copyFileToProject } from './lib/files'; 12 | import midi from './lib/midi'; 13 | import scheduler from './lib/scheduler'; 14 | import audioGraph from './lib/audio-graph'; 15 | import store from './lib/store'; 16 | import { readConfig, persistStorePeriodically } from './lib/files'; 17 | 18 | class Editor extends Component { 19 | 20 | componentDidMount() { 21 | const { params: { project_id } } = this.props; 22 | const config = readConfig(project_id); 23 | 24 | // initialize the editor 25 | this.props.initEditor(config); 26 | 27 | // initialize everything under the hood 28 | audioGraph.init(store); 29 | scheduler.init(store); 30 | midi.init(store, scheduler.handleManualSchedule, scheduler.scheduleRow); 31 | 32 | // persist project config 33 | persistStorePeriodically(project_id, store); 34 | } 35 | 36 | render() { 37 | const { children, onDrop, params: { project_id } } = this.props; 38 | return ( 39 |
40 | 41 |
42 |
43 | 44 | Pads 45 | 46 | 47 | Tracks 48 | 49 | 50 | Files 51 | 52 | 53 | Project 54 | 55 |
56 |
57 | { children } 58 |
59 |
60 | 61 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | const mapDispatchToProps = (dispatch, props) => ({ 68 | onDrop: (filePath) => { 69 | const id = uuid.v4(); 70 | const { params: { project_id } } = props; 71 | copyFileToProject(filePath, id, project_id) 72 | .then((file) => dispatch(addFile(id, file))); 73 | }, 74 | 75 | initEditor: (config) => { 76 | dispatch({ type: 'init', state: config }); 77 | } 78 | }); 79 | 80 | export default connect(null, mapDispatchToProps)(Editor); 81 | -------------------------------------------------------------------------------- /src/data/clips.js: -------------------------------------------------------------------------------- 1 | /** 2 | * -------------------- ACTION TYPES ---------------------------- 3 | */ 4 | const CHANGE_CLIP_FIELD = 'jsconf2017/clips/CHANGE_CLIP_FIELD'; 5 | export const CREATE_CLIP = 'jsconf2017/clips/CREATE_CLIP'; 6 | export const DELETE_CLIP = 'jsconf2017/clips/DELETE_CLIP'; 7 | 8 | export const CLIP_TYPE_NONE = 'select a type'; 9 | export const CLIP_TYPE_AUDIO_SAMPLE = 'audiosample'; 10 | export const CLIP_TYPE_AUDIO_AND_VIDEO = 'audioandvideo'; 11 | export const CLIP_TYPE_VIDEO = 'video'; 12 | export const CLIP_TYPES = [CLIP_TYPE_NONE, CLIP_TYPE_AUDIO_SAMPLE, CLIP_TYPE_AUDIO_AND_VIDEO, CLIP_TYPE_VIDEO]; 13 | export const AUDIO_BEHAVIOR_SCHEDULABLE = 'schedulable'; 14 | export const AUDIO_BEHAVIOR_SINGLE = 'single'; 15 | export const AUDIO_BEHAVIOR_TYPES = [AUDIO_BEHAVIOR_SCHEDULABLE, AUDIO_BEHAVIOR_SINGLE]; 16 | 17 | const BASE_AUDIO_SAMPLE = { 18 | gain: 1, 19 | behavior: AUDIO_BEHAVIOR_SCHEDULABLE, 20 | file: '', 21 | loop: true, 22 | track: 'master' 23 | }; 24 | 25 | const BASE_VIDEO_SAMPLE = { 26 | noFilter: false 27 | }; 28 | 29 | const BASE_AUDIO_VIDEO_SAMPLE = { 30 | ...BASE_AUDIO_SAMPLE, 31 | videoFile: '' 32 | }; 33 | 34 | /** 35 | * -------------------- REDUCER ---------------------------- 36 | */ 37 | export default function clips(state = {}, action) { 38 | const id = action.id; 39 | 40 | switch (action.type) { 41 | case CHANGE_CLIP_FIELD: 42 | const { field, value } = action; 43 | let clip = state[id]; 44 | 45 | // make sure, the clip has the base values for its type 46 | if (clip && field === 'type') { 47 | switch (value) { 48 | case CLIP_TYPE_AUDIO_SAMPLE: 49 | clip = { 50 | ...BASE_AUDIO_SAMPLE, 51 | ...clip 52 | }; 53 | break; 54 | case CLIP_TYPE_VIDEO: 55 | clip = { 56 | ...BASE_VIDEO_SAMPLE, 57 | ...clip 58 | }; 59 | break; 60 | case CLIP_TYPE_AUDIO_AND_VIDEO: 61 | clip ={ 62 | ...BASE_AUDIO_VIDEO_SAMPLE, 63 | ...clip 64 | }; 65 | break; 66 | default: 67 | } 68 | 69 | } 70 | 71 | return { 72 | ...state, 73 | [id]: { ...clip, [field]: value } 74 | }; 75 | case CREATE_CLIP: 76 | return { 77 | ...state, 78 | [id]: { id } 79 | } 80 | case DELETE_CLIP: 81 | const clipsCopy = { ...state }; 82 | delete clipsCopy[id]; 83 | return clipsCopy; 84 | default: 85 | return state; 86 | } 87 | } 88 | 89 | /** 90 | * -------------------- ACTION CREATORS ---------------------------- 91 | */ 92 | export const changeClipField = (id, field, value) => ({ type: CHANGE_CLIP_FIELD, id, field, value }); 93 | export const createClip = (x, y, id, padId) => ({ type: CREATE_CLIP, x, y, id, padId }); 94 | export const deleteClip = (id, padId) => ({ type: DELETE_CLIP, id, padId }); 95 | -------------------------------------------------------------------------------- /src/tracks/track-editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux' 3 | import { 4 | changeTrackGain, 5 | changeTrackName, 6 | removeTrack 7 | } from '../data/tracks'; 8 | 9 | import './track-editor.css'; 10 | 11 | class TrackEditor extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | editingName: false 17 | }; 18 | } 19 | 20 | shouldComponentUpdate(nextProps, nextState) { 21 | return nextProps.track !== this.props.track 22 | || nextState.editingName !== this.state.editingName; 23 | } 24 | 25 | render() { 26 | const { editingName } = this.state; 27 | const { track: { gain, name } } = this.props; 28 | 29 | return ( 30 |
31 | { editingName && ( 32 | this.nameInput = input} 34 | type="text" 35 | onChange={this.onChangeText} 36 | onBlur={this.leaveEditMode} 37 | value={name} 38 | placeholder="Name the track..." 39 | className="trackEditor__nameInput" 40 | /> 41 | )} 42 | 43 | { !editingName && ( 44 |

{name}

48 | )} 49 | 50 | 58 | 59 |
60 | 61 | ); 62 | } 63 | 64 | enterEditMode = () => { 65 | this.setState({ editingName: true }, () => this.nameInput.focus()); 66 | } 67 | 68 | leaveEditMode = () => { 69 | this.setState({ editingName: false }); 70 | } 71 | 72 | onChangeText = (event) => { 73 | this.props.changeTrackName(event.target.value); 74 | } 75 | 76 | changeGain = (event) => { 77 | const gain = parseFloat(event.target.value, 10); 78 | this.props.changeTrackGain(gain); 79 | } 80 | 81 | removeTrack = () => { 82 | const { trackId, clips } = this.props; 83 | 84 | if (trackId === 'master') { 85 | return alert('You cannot remove the master track'); 86 | } 87 | 88 | const associatedClip = Object 89 | .keys(clips) 90 | .find((clipId) => clips[clipId].track === trackId); 91 | 92 | if (associatedClip) { 93 | return alert('The track has still clips associated to it'); 94 | } 95 | 96 | this.props.removeTrack(); 97 | } 98 | } 99 | 100 | const mapStateToProps = (state, ownProps) => ({ 101 | track: state.tracks[ownProps.trackId], 102 | clips: state.clips 103 | }); 104 | 105 | const mapDispatchToProps = (dispatch, ownProps) => ({ 106 | changeTrackName(name) { dispatch(changeTrackName(ownProps.trackId, name)) }, 107 | changeTrackGain(gain) { dispatch(changeTrackGain(ownProps.trackId, gain)) }, 108 | removeTrack() { dispatch(removeTrack(ownProps.trackId)) } 109 | }); 110 | 111 | export default connect(mapStateToProps, mapDispatchToProps)(TrackEditor); 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsconf-2017", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "autoprefixer": "6.5.1", 7 | "babel-core": "6.17.0", 8 | "babel-eslint": "7.0.0", 9 | "babel-jest": "16.0.0", 10 | "babel-loader": "6.2.5", 11 | "babel-preset-react-app": "^1.0.0", 12 | "case-sensitive-paths-webpack-plugin": "1.1.4", 13 | "chalk": "1.1.3", 14 | "connect-history-api-fallback": "1.3.0", 15 | "cross-spawn": "4.0.2", 16 | "css-loader": "0.25.0", 17 | "detect-port": "1.0.1", 18 | "dotenv": "2.0.0", 19 | "electron-packager": "8.3.0", 20 | "electron-prebuilt": "1.4.10", 21 | "eslint": "3.8.1", 22 | "eslint-config-react-app": "^0.3.0", 23 | "eslint-loader": "1.6.0", 24 | "eslint-plugin-flowtype": "2.21.0", 25 | "eslint-plugin-import": "2.0.1", 26 | "eslint-plugin-jsx-a11y": "2.2.3", 27 | "eslint-plugin-react": "6.4.1", 28 | "express": "4.14.0", 29 | "extract-text-webpack-plugin": "1.0.1", 30 | "file-loader": "0.9.0", 31 | "filesize": "3.3.0", 32 | "find-cache-dir": "0.1.1", 33 | "fs-extra": "0.30.0", 34 | "gzip-size": "3.0.0", 35 | "html-webpack-plugin": "2.24.0", 36 | "http-proxy-middleware": "0.17.2", 37 | "jest": "16.0.2", 38 | "json-loader": "0.5.4", 39 | "npm-run-all": "^4.0.2", 40 | "object-assign": "4.1.0", 41 | "path-exists": "2.1.0", 42 | "portfinder": "1.0.10", 43 | "postcss-cssnext": "2.9.0", 44 | "postcss-import": "9.0.0", 45 | "postcss-loader": "1.0.0", 46 | "promise": "7.1.1", 47 | "react-dev-utils": "^0.3.0", 48 | "recursive-readdir": "2.1.0", 49 | "rimraf": "2.5.4", 50 | "strip-ansi": "3.0.1", 51 | "style-loader": "0.13.1", 52 | "url-loader": "0.5.7", 53 | "webpack": "1.13.2", 54 | "webpack-dev-server": "1.16.2", 55 | "webpack-manifest-plugin": "1.1.0", 56 | "whatwg-fetch": "1.0.0" 57 | }, 58 | "dependencies": { 59 | "dat.gui": "^0.6.1", 60 | "dilla": "1.8.3", 61 | "electron-log": "1.2.2", 62 | "event-emitter": "0.3.4", 63 | "file-saver": "1.3.3", 64 | "jszip": "3.1.3", 65 | "lodash": "4.17.2", 66 | "normalize.css": "5.0.0", 67 | "react": "15.4.0", 68 | "react-dom": "15.4.0", 69 | "react-redux": "4.4.6", 70 | "react-router": "^3.0.0", 71 | "recompose": "0.20.2", 72 | "redux": "3.6.0", 73 | "redux-logger": "2.7.4", 74 | "reselect": "2.5.4", 75 | "three": "^0.84.0", 76 | "uuid": "2.0.3", 77 | "web-midi": "2.0.1" 78 | }, 79 | "scripts": { 80 | "start": "run-p start:*", 81 | "build": "node scripts/build.js", 82 | "test": "node scripts/test.js --env=jsdom", 83 | "start:server": "node scripts/start.js", 84 | "start:electron": "electron scripts/start-electron.js", 85 | "build-electron": "rm -rf ./build && yarn run build && node scripts/build-electron.js" 86 | }, 87 | "jest": { 88 | "moduleFileExtensions": [ 89 | "jsx", 90 | "js", 91 | "json" 92 | ], 93 | "moduleNameMapper": { 94 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js", 95 | "^.+\\.css$": "/config/jest/CSSStub.js" 96 | }, 97 | "setupFiles": [ 98 | "/config/polyfills.js" 99 | ], 100 | "testPathIgnorePatterns": [ 101 | "/(build|docs|node_modules)/" 102 | ], 103 | "testEnvironment": "node" 104 | }, 105 | "babel": { 106 | "presets": [ 107 | "react-app" 108 | ] 109 | }, 110 | "eslintConfig": { 111 | "extends": "react-app" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/lib/loader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | import log from 'electron-log'; 6 | import audioContext from './audio/context'; 7 | import { fileLoaded } from '../data/file-loader'; 8 | import { getProjectPath } from './files'; 9 | import { isAudio, isVideo } from './regular-expressions'; 10 | 11 | const loaderContainerStyle = { 12 | position: 'fixed', 13 | left: 0, 14 | right: 0, 15 | bottom: 0 16 | }; 17 | 18 | class Loader extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = {}; 23 | } 24 | 25 | shouldComponentUpdate(props) { 26 | return Object.keys(props.files).length !== Object.keys(this.props.files).length 27 | || Object.keys(props.fileLoader).length !== Object.keys(this.props.fileLoader).length 28 | } 29 | 30 | componentDidMount() { 31 | const { files } = this.props; 32 | 33 | Object.keys(files).forEach((fileId) => { 34 | this.loadFile(files[fileId], fileId); 35 | }); 36 | } 37 | 38 | componentWillUpdate(nextProps) { 39 | const { files, fileLoader } = this.props; 40 | if (nextProps.files !== files) { 41 | Object 42 | .keys(nextProps.files) 43 | .filter((fileId) => { 44 | return !fileLoader[fileId]; 45 | }) 46 | .forEach((fileId) => { 47 | this.loadFile(nextProps.files[fileId], fileId); 48 | }); 49 | } 50 | } 51 | 52 | componentWillReceiveProps(nextProps) { 53 | const { files, fileLoader } = nextProps; 54 | const totalFiles = Object.keys(files).length; 55 | const loadedFiles = Object.keys(fileLoader).length; 56 | const progress = loadedFiles / totalFiles; 57 | const done = progress === 1; 58 | this.setState({ 59 | progress, done 60 | }); 61 | } 62 | 63 | render() { 64 | const { progress } = this.state; 65 | 66 | const style = { 67 | backgroundColor: '#bada55', 68 | height: '5px', 69 | opacity: progress === 1 ? 0 : 1, 70 | transition: 'width 0.5s ease-in-out, opacity 1s ease-out', 71 | width: `${progress * 100}%` 72 | } 73 | 74 | return ( 75 |
76 |
77 |
78 | ); 79 | } 80 | 81 | loadFile(file, id) { 82 | if (isAudio.test(file.location)) { 83 | this.loadAudioFile(file, id); 84 | } else if (isVideo.test(file.location)) { 85 | this.loadVideoFile(file, id); 86 | } 87 | } 88 | 89 | loadAudioFile(file, id) { 90 | const filePath = path.join(getProjectPath(this.props.projectId), file.location); 91 | return new Promise((resolve, reject) => { 92 | resolve(fs.readFileSync(filePath)); 93 | }) 94 | .then((fileBuffer) => fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength)) 95 | .then((arrayBuffer) => { 96 | return new Promise((resolve, reject) => { 97 | audioContext.decodeAudioData(arrayBuffer, resolve, reject); 98 | }); 99 | }) 100 | .then((audioBuffer) => { 101 | this.props.dispatch(fileLoaded(id, audioBuffer)); 102 | }).catch((error) => { 103 | log.error('could not load audio file: ' + filePath); 104 | log.error(error.toString()); 105 | }); 106 | } 107 | 108 | loadVideoFile(file, id) { 109 | const filePath = path.join(getProjectPath(this.props.projectId), file.location); 110 | 111 | return new Promise((resolve, reject) => { 112 | const data = fs.readFileSync(filePath); 113 | const blob = new Blob([data], {type: 'video/mp4'}); 114 | const videoElement = document.createElement('video'); 115 | videoElement.preload = 'auto'; 116 | videoElement.src = URL.createObjectURL(blob); 117 | videoElement.load(); 118 | 119 | // FIXME: should wait for the video::canplaythrough-event (although 120 | // it probably won't matter using localhost-networking) 121 | this.props.dispatch(fileLoaded(id, videoElement)); 122 | resolve(); 123 | }); 124 | } 125 | 126 | } 127 | 128 | const mapStateToProps = (state) => ({ 129 | files: state.files, 130 | fileLoader: state.fileLoader 131 | }); 132 | 133 | export default connect(mapStateToProps)(Loader); 134 | -------------------------------------------------------------------------------- /src/lib/files.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | import log from 'electron-log'; 3 | import _ from 'lodash'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import JSZip from 'jszip'; 7 | import { saveAs } from 'file-saver'; 8 | 9 | let _userDataDirectory = remote.getGlobal('userDataDirectory'); 10 | let _userProjectDirectory = path.join(_userDataDirectory, 'projects'); 11 | 12 | try { 13 | fs.mkdirSync(_userDataDirectory); 14 | fs.mkdirSync(_userProjectDirectory); 15 | log.info('Created user data directory'); 16 | } catch(e) { 17 | log.info('User directory already exists'); 18 | } 19 | 20 | export const userDataDirectory = _userDataDirectory; 21 | export const userProjectDirectory = _userProjectDirectory; 22 | 23 | export function getProjectPath (projectId) { 24 | return path.join(userProjectDirectory, projectId); 25 | } 26 | 27 | export function getConfigPath (projectId) { 28 | return path.join(getProjectPath(projectId), 'project.json'); 29 | } 30 | 31 | export function readConfig (projectId) { 32 | const configPath = getConfigPath(projectId); 33 | return JSON.parse(fs.readFileSync(configPath).toString()); 34 | } 35 | 36 | export function persistStorePeriodically (projectId, store) { 37 | store.subscribe(_.throttle(() => { 38 | const filteredConfig = _.omit(store.getState(), ['scheduler', 'fileLoader', 'controllers']); 39 | saveAsProjectConfig(projectId, filteredConfig); 40 | }, 5000)); 41 | } 42 | 43 | export function saveAsProjectConfig (projectId, projectConfig) { 44 | const configPath = getConfigPath(projectId); 45 | const configString = JSON.stringify(projectConfig, null, 2); 46 | fs.writeFileSync(configPath, configString); 47 | } 48 | 49 | export function saveProjectAsZip (projectId) { 50 | const projectPath = getProjectPath(projectId); 51 | const files = fs.readdirSync(projectPath); 52 | const zip = new JSZip(); 53 | 54 | const readFileOperations = files.map((file) => 55 | readFile(path.join(projectPath, file)) 56 | .then((data) => zip.file(file, data)) 57 | ); 58 | 59 | Promise 60 | .all(readFileOperations) 61 | .then(() => zip.generateAsync({ type: 'blob'})) 62 | .then((blob) => saveAs(blob, `${projectId}.zip`)); 63 | } 64 | 65 | function readFile (filePath) { 66 | return new Promise((resolve, reject) => { 67 | fs.readFile(filePath, function(err, data){ 68 | if (err) { 69 | reject(err); 70 | } else { 71 | resolve(data); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | export function importProjectFromZip (zipPath) { 78 | const projectName = zipPath.split(path.sep).pop().replace('.zip', ''); 79 | let projectPath = getProjectPath(projectName); 80 | let counter = 1; 81 | 82 | // make sure not to overwrite a previous project with the same name 83 | while (fs.existsSync(projectPath)) { 84 | projectPath = `${getProjectPath(projectName)}-${counter++}`; 85 | } 86 | 87 | fs.mkdirSync(projectPath); 88 | 89 | const zip = new JSZip(); 90 | const fileContent = fs.readFileSync(zipPath); 91 | return zip.loadAsync(fileContent) 92 | .then(() => { 93 | const promises = []; 94 | zip.forEach((filePath, file) => { 95 | promises.push( 96 | new Promise((resolve) => { 97 | file 98 | .nodeStream() 99 | .pipe(fs.createWriteStream(path.join(projectPath, filePath))) 100 | .on('finish', resolve); 101 | }) 102 | ); 103 | }); 104 | return Promise.all(promises); 105 | }); 106 | } 107 | 108 | export function copyFileToProject (filePath, newFileId, projectId) { 109 | return readFile(filePath) 110 | .then((fileData) => new Promise((resolve, reject) => { 111 | const fileName = filePath.split(path.sep).pop(); 112 | const newFileName = `${newFileId}-${fileName}`; 113 | const newFilePath = path.join(getProjectPath(projectId), newFileName); 114 | fs.writeFileSync(newFilePath, fileData); 115 | resolve({name: fileName, location: newFileName}); 116 | })) 117 | } 118 | 119 | export function deleteFile (projectId, fileLocation) { 120 | const filePath = path.join(getProjectPath(projectId), fileLocation); 121 | fs.unlink(filePath); 122 | } 123 | -------------------------------------------------------------------------------- /src/data/pads.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | import { CREATE_CLIP, DELETE_CLIP } from './clips'; 3 | 4 | /** 5 | * -------------------- ACTION TYPES ---------------------------- 6 | */ 7 | const SELECT_CLIP = 'jsconf2017/pads/SELECT_CLIP'; 8 | const CREATE_PAD = 'jsconf2017/pads/CREATE_PAD'; 9 | const REMOVE_PAD = 'jsconf2017/pads/REMOVE_PAD'; 10 | 11 | /** 12 | * -------------------- REDUCER ---------------------------- 13 | */ 14 | export default function pads(state = {}, action) { 15 | const { padId } = action; 16 | switch (action.type) { 17 | case CREATE_CLIP: 18 | case SELECT_CLIP: 19 | case DELETE_CLIP: 20 | const pad = state[padId]; 21 | return { 22 | ...state, 23 | [padId]: padReducer(pad, action) 24 | }; 25 | case CREATE_PAD: 26 | const id = uuid.v4(); 27 | return { 28 | ...state, 29 | [id]: generateEmptyPad(id) 30 | }; 31 | case REMOVE_PAD: 32 | const copiedPads = { ...state }; 33 | delete copiedPads[padId]; 34 | return copiedPads; 35 | default: 36 | return state; 37 | } 38 | } 39 | 40 | function padReducer(state, action) { 41 | switch (action.type) { 42 | case CREATE_CLIP: { 43 | const { clips } = state; 44 | const { x, y, id } = action; 45 | 46 | return { 47 | ...state, 48 | clips: changeValueAtPoint({ 49 | array2d: clips, 50 | value: id, 51 | x, y 52 | }) 53 | }; 54 | } 55 | 56 | case SELECT_CLIP: { 57 | return { 58 | ...state, 59 | selectedClipId: action.selectedClipId 60 | }; 61 | } 62 | 63 | case DELETE_CLIP: { 64 | const { clips } = state; 65 | const { id } = action; 66 | const coordinates = findIn2dArray(clips, id); 67 | 68 | if (!coordinates) { return state; } 69 | 70 | const { x, y } = coordinates; 71 | 72 | return { 73 | ...state, 74 | clips: changeValueAtPoint({ 75 | array2d: clips, 76 | value: null, 77 | x, y 78 | }) 79 | }; 80 | } 81 | default: 82 | return state; 83 | } 84 | } 85 | 86 | /** 87 | * -------------------- ACTION CREATORS ---------------------------- 88 | */ 89 | export const selectClip = (selectedClipId, padId) => ({ type: SELECT_CLIP, selectedClipId, padId }); 90 | 91 | export const createPad = () => ({ type: CREATE_PAD }); 92 | 93 | export const removePad = (padId) => ({ type: REMOVE_PAD, padId }); 94 | 95 | /** 96 | * -------------------- HELPERS ---------------------------- 97 | */ 98 | function findIn2dArray(array2d, value) { 99 | for (var y = 0; y < 8; y++) { 100 | for (var x = 0; x < array2d.length; x++) { 101 | if (array2d[y][x] === value) { 102 | return { x, y }; 103 | } 104 | } 105 | } 106 | return null; 107 | } 108 | 109 | function changeValueAtPoint({x, y, value, array2d}){ 110 | const newRow = [ 111 | ...array2d[y].slice(0, x), 112 | value, 113 | ...array2d[y].slice(x + 1) 114 | ]; 115 | 116 | return array2d.slice(0, y) 117 | .concat([newRow]) 118 | .concat(array2d.slice(y + 1)); 119 | } 120 | 121 | function generateEmptyPad(id) { 122 | return { 123 | id, 124 | clips: [ 125 | [ 126 | null, 127 | null, 128 | null, 129 | null, 130 | null, 131 | null, 132 | null, 133 | null 134 | ], 135 | [ 136 | null, 137 | null, 138 | null, 139 | null, 140 | null, 141 | null, 142 | null, 143 | null 144 | ], 145 | [ 146 | null, 147 | null, 148 | null, 149 | null, 150 | null, 151 | null, 152 | null, 153 | null 154 | ], 155 | [ 156 | null, 157 | null, 158 | null, 159 | null, 160 | null, 161 | null, 162 | null, 163 | null 164 | ], 165 | [ 166 | null, 167 | null, 168 | null, 169 | null, 170 | null, 171 | null, 172 | null, 173 | null 174 | ], 175 | [ 176 | null, 177 | null, 178 | null, 179 | null, 180 | null, 181 | null, 182 | null, 183 | null 184 | ], 185 | [ 186 | null, 187 | null, 188 | null, 189 | null, 190 | null, 191 | null, 192 | null, 193 | null 194 | ], 195 | [ 196 | null, 197 | null, 198 | null, 199 | null, 200 | null, 201 | null, 202 | null, 203 | null 204 | ] 205 | ], 206 | selectedClipId: null 207 | }; 208 | } 209 | -------------------------------------------------------------------------------- /src/projects.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { Link } from 'react-router'; 5 | import log from 'electron-log'; 6 | import { shell } from 'electron'; 7 | import { not } from './lib/junk'; 8 | import { userProjectDirectory, importProjectFromZip } from './lib/files'; 9 | import blankProject from './data/blank_project.json'; 10 | const { dialog } = require('electron').remote; 11 | 12 | import './projects.css' 13 | 14 | export default class Projects extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = { 19 | projects: null, 20 | importing: false, 21 | showNewForm: false 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | this.syncDirectories(); 27 | } 28 | 29 | render() { 30 | const { projects, showNewForm, importing } = this.state; 31 | const notReady = !projects; 32 | const noProjects = !notReady && projects.length === 0; 33 | const hasProjects = !!projects && projects.length > 0; 34 | const importButtonLabel = importing ? 'Importing...' : 'Import project'; 35 | 36 | return ( 37 |
38 | { notReady && ( 39 |

{'Scanning for projects...'}

40 | )} 41 | 42 | { noProjects && ( 43 |

{'No projects yet'}

44 | )} 45 | 46 | { hasProjects && ( 47 |
    48 | { projects.map((projectId, index) => 49 |
  • 50 | {projectId} 51 |
  • 52 | )} 53 |
54 | )} 55 | 56 |
57 | { showNewForm && ( 58 |
59 | this.input = input} 63 | type="text" 64 | placeholder="Enter project name" 65 | /> 66 |
67 | )} 68 | 69 | 75 | 82 | 88 |
89 |
90 | ); 91 | } 92 | 93 | showNewProjectForm = () => { 94 | this.setState({ 95 | showNewForm: true 96 | }); 97 | } 98 | 99 | createNewProject = (event) => { 100 | event.preventDefault(); 101 | const projectName = this.input.value; 102 | const projectPath = path.join(userProjectDirectory, projectName); 103 | const projectExists = fs.existsSync(projectPath); 104 | 105 | if (projectName && !projectExists) { 106 | const projectFilePath = path.join(projectPath, 'project.json'); 107 | fs.mkdirSync(projectPath); 108 | fs.writeFileSync(projectFilePath, JSON.stringify(blankProject, null, 2)); 109 | this.syncDirectories(); 110 | this.setState({ 111 | showNewForm: false 112 | }); 113 | } 114 | } 115 | 116 | importProject = (event) => { 117 | event.preventDefault(); 118 | const files = dialog.showOpenDialog({ 119 | properties: ['openFile'], 120 | filters: [ 121 | { name: 'Zip files', extensions: ['zip'] } 122 | ] 123 | }); 124 | if (files) { 125 | const file = files.pop(); 126 | this.setState({ importing: true }); 127 | importProjectFromZip(file) 128 | .then(() => this.setState({ importing: false })) 129 | .then(() => this.syncDirectories()); 130 | } 131 | } 132 | 133 | syncDirectories() { 134 | fs.readdir(userProjectDirectory, (err, dirs) => { 135 | if (err) { 136 | this.createProjectsDirectory(); 137 | this.setState({ projects: [] }); 138 | } else { 139 | this.setState({ projects: dirs.filter(not) }) 140 | } 141 | }); 142 | } 143 | 144 | createProjectsDirectory() { 145 | fs.mkdir(userProjectDirectory, (err) => { 146 | if (err) { 147 | const message = 'Could not create projects directory: ' + err.toString(); 148 | log.error(message); 149 | alert(message); 150 | } 151 | }); 152 | } 153 | 154 | openProjectsFolder = () => { 155 | shell.openItem(userProjectDirectory); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /scripts/start-electron.js: -------------------------------------------------------------------------------- 1 | // prevent any non-us locale: https://github.com/electron/electron/issues/8825 2 | process.env.LC_ALL = 'en_US'; 3 | 4 | const { app, BrowserWindow } = require('electron'); 5 | const portfinder = require('portfinder'); 6 | const express = require('express'); 7 | const log = require('electron-log'); 8 | const path = require('path'); 9 | 10 | let isProduction = false; 11 | try { 12 | require('./prod.json'); 13 | isProduction = true; 14 | log.info('running in prod mode'); 15 | } catch (e) { 16 | log.info('running in dev mode'); 17 | } 18 | 19 | global.userDataDirectory = path.join(userData(), 'jsconf-2017'); 20 | log.info('user data dir: ' + global.userDataDirectory); 21 | 22 | // Keep a global reference of the window object, if you don't, the window will 23 | // be closed automatically when the JavaScript object is garbage collected. 24 | let win; 25 | 26 | function createWindow () { 27 | // Create the browser window. 28 | win = new BrowserWindow({ 29 | width: 960, 30 | height: 800, 31 | title: 'JSConf 2017' 32 | }); 33 | 34 | if (isProduction) { 35 | log.info('trying to find a port'); 36 | portfinder.getPort(function (err, port) { 37 | if (err) { 38 | log.error('Could not find a port'); 39 | log.error(err.toString()); 40 | } 41 | const expressApp = express(); 42 | expressApp.use(express.static(__dirname)); 43 | log.info('static path ' + __dirname); 44 | expressApp.use((_, res) => res.redirect('/')); 45 | 46 | expressApp.listen(port, function (a,b) { 47 | 48 | log.info('JSConf 2017 is running on port', port); 49 | 50 | // and load the index.html of the app. 51 | win.loadURL('http://localhost:' + port); 52 | }); 53 | }); 54 | } else { 55 | win.loadURL('http://localhost:3000'); 56 | } 57 | 58 | // Emitted when the window is closed. 59 | win.on('closed', () => { 60 | // Dereference the window object, usually you would store windows 61 | // in an array if your app supports multi windows, this is the time 62 | // when you should delete the corresponding element. 63 | win = null 64 | }) 65 | } 66 | 67 | // This method will be called when Electron has finished 68 | // initialization and is ready to create browser windows. 69 | // Some APIs can only be used after this event occurs. 70 | app.on('ready', createWindow); 71 | 72 | // Quit when all windows are closed. 73 | app.on('window-all-closed', () => { 74 | // On macOS it is common for applications and their menu bar 75 | // to stay active until the user quits explicitly with Cmd + Q 76 | if (process.platform !== 'darwin') { 77 | app.quit() 78 | } 79 | }); 80 | 81 | app.on('activate', () => { 82 | // On macOS it's common to re-create a window in the app when the 83 | // dock icon is clicked and there are no other windows open. 84 | if (win === null) { 85 | createWindow() 86 | } 87 | }); 88 | 89 | /** 90 | * Had to import this helper from https://github.com/MrJohz/appdirectory and change it because of some issues on windows 91 | * Thei licence applies 92 | Copyright (c) 2014 Johz jonathan.frere@gmail.com 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 95 | 96 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 97 | 98 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 99 | */ 100 | function userData (roaming, platform) { 101 | var dataPath; 102 | platform = platform || process.platform 103 | if (platform === "darwin") { 104 | dataPath = path.join(process.env.HOME, 'Library', 'Application Support') 105 | } else if (platform === "win32") { 106 | var sysVariable 107 | if (roaming) { 108 | sysVariable = "APPDATA" 109 | } else { 110 | sysVariable = "LOCALAPPDATA" // Note, on WinXP, LOCALAPPDATA doesn't exist, catch this later 111 | } 112 | dataPath = path.join(process.env[sysVariable] || process.env.APPDATA /*catch for XP*/) 113 | } else { 114 | if (process.env.XDG_DATA_HOME) { 115 | dataPath = path.join(process.env.XDG_DATA_HOME) 116 | } else { 117 | dataPath = path.join(process.env.HOME, ".local", "share") 118 | } 119 | } 120 | return dataPath 121 | } 122 | -------------------------------------------------------------------------------- /src/lib/video/renderer.js: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | PerspectiveCamera, 4 | PlaneBufferGeometry, 5 | Points, 6 | Scene, 7 | ShaderMaterial, 8 | Texture, 9 | Vector3, 10 | WebGLRenderer 11 | } from 'three'; 12 | 13 | import dotMatrixShader from './dot-matrix-shader.js'; 14 | 15 | export default class VideoRenderer { 16 | constructor() { 17 | this.animationLoopRunning = false; 18 | this.videos = []; 19 | 20 | this.resolution = '160x90'; 21 | this.geometryCache = {}; 22 | 23 | this.initTexture(); 24 | this.initRenderer(); 25 | 26 | this.animationLoopTick = this.animationLoopTick.bind(this); 27 | } 28 | 29 | setVideos(videos = []) { 30 | this.videos = videos; 31 | 32 | if (this.videos.length === 0) { 33 | this.stop(); 34 | } else { 35 | this.start(); 36 | } 37 | } 38 | 39 | setRenderParams(renderParams) { 40 | this.renderer.setClearColor(renderParams.backgroundColor, 1); 41 | this.setResolution(renderParams.resolution); 42 | this.updateUniforms({ 43 | pointColor: renderParams.foregroundColor, 44 | size: renderParams.pointSize, 45 | lumMin: renderParams.luminanceMin, 46 | lumMax: renderParams.luminanceMax, 47 | r0: renderParams.r0 48 | }); 49 | } 50 | 51 | getDomElement() { 52 | return this.renderer.domElement; 53 | } 54 | 55 | /** 56 | * @private 57 | */ 58 | start() { 59 | if (this.animationLoopRunning) { return; } 60 | 61 | this.animationLoopRunning = true; 62 | this.nextFrameId = requestAnimationFrame(this.animationLoopTick) 63 | } 64 | 65 | /** 66 | * @private 67 | */ 68 | stop() { 69 | cancelAnimationFrame(this.nextFrameId); 70 | this.animationLoopRunning = false; 71 | } 72 | 73 | /** 74 | * @private 75 | */ 76 | animationLoopTick() { 77 | const activeVideos = this.videos; 78 | const textureCanvas = this.texture.image; 79 | 80 | const n = activeVideos.length; 81 | const ctx = textureCanvas.getContext('2d'); 82 | const w = textureCanvas.width; 83 | const h = textureCanvas.height; 84 | 85 | if (n === 0) { 86 | ctx.clearRect(0, 0, w, h); 87 | } else { 88 | activeVideos.forEach((video, index) => { 89 | const dstSliceWidth = w / n; 90 | 91 | // FIXME: only use a portion of the source-video to prevent distortion 92 | ctx.drawImage(video, index * dstSliceWidth, 0, dstSliceWidth, h); 93 | }); 94 | } 95 | 96 | this.render(); 97 | 98 | if (this.animationLoopRunning) { 99 | this.nextFrameId = requestAnimationFrame(this.animationLoopTick); 100 | } 101 | } 102 | 103 | /** 104 | * @private 105 | */ 106 | render() { /* stub: will be overwritten in initRenderer */ } 107 | 108 | /** 109 | * @private 110 | */ 111 | updateUniforms(values) { 112 | const { uniforms } = this.points.material; 113 | 114 | Object.keys(values).forEach(name => { 115 | const uniform = uniforms[name]; 116 | if (!uniform) { return; } 117 | 118 | if (uniform.value instanceof Color) { 119 | uniform.value.set(values[name]); 120 | } else { 121 | uniform.value = values[name]; 122 | } 123 | }); 124 | } 125 | 126 | /** 127 | * @private 128 | */ 129 | initTexture() { 130 | const canvas = document.createElement('canvas'); 131 | 132 | canvas.width = 512; 133 | canvas.height = 256; 134 | 135 | // FIXME: configure texture 136 | this.texture = new Texture(canvas); 137 | } 138 | 139 | /** 140 | * @private 141 | */ 142 | setResolution(resolution) { 143 | if (resolution === this.resolution) { return; } 144 | 145 | this.points.geometry = this.getGeometry(resolution); 146 | this.resolution = resolution; 147 | } 148 | 149 | /** 150 | * @private 151 | */ 152 | getGeometry(resolution) { 153 | if (this.geometryCache[resolution]) { 154 | return this.geometryCache[resolution]; 155 | } 156 | 157 | const [xRes, yRes] = resolution.split('x'); 158 | const geometry = new PlaneBufferGeometry(16, 9, xRes, yRes); 159 | 160 | this.geometryCache[resolution] = geometry; 161 | 162 | return geometry; 163 | } 164 | 165 | /** 166 | * @private 167 | */ 168 | initRenderer() { 169 | const renderer = new WebGLRenderer({ alpha: true, antialias: true }); 170 | renderer.setSize(window.innerWidth, window.innerHeight); 171 | renderer.setClearColor(0xe10079); 172 | 173 | const scene = new Scene(); 174 | // FIXME: maybe we should just use an orthographic camera here? 175 | const camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); 176 | camera.position.set(0, 0, 6.3); 177 | camera.lookAt(new Vector3(0, 0, 0)); 178 | 179 | const material = new ShaderMaterial(dotMatrixShader); 180 | material.uniforms.texture.value = this.texture; 181 | material.uniforms.scale.value = window.innerHeight / 2; 182 | 183 | // FIXME: resolution and size might be configurable 184 | const points = new Points( 185 | this.getGeometry(this.resolution), 186 | material); 187 | 188 | this.points = points; 189 | scene.add(points); 190 | 191 | // bind events 192 | window.addEventListener('resize', () => { 193 | renderer.setSize(window.innerWidth, window.innerHeight); 194 | camera.aspect = window.innerWidth / window.innerHeight; 195 | camera.updateProjectionMatrix(); 196 | material.uniforms.scale.value = window.innerHeight / 2; 197 | }); 198 | 199 | // finalize 200 | this.renderer = renderer; 201 | this.render = () => { 202 | this.texture.needsUpdate = true; 203 | 204 | renderer.render(scene, camera); 205 | } 206 | } 207 | } -------------------------------------------------------------------------------- /src/lib/midi.js: -------------------------------------------------------------------------------- 1 | import midi from '../vendor/web-midi'; 2 | 3 | import { 4 | addController, 5 | mapControllerToPad, 6 | removeController 7 | } from '../data/controllers'; 8 | 9 | import { 10 | isPlaying 11 | } from '../data/scheduler'; 12 | 13 | import { 14 | setRenderparam 15 | } from '../data/video-renderer'; 16 | 17 | const COLOR_CODES = { 18 | OFF: 12, 19 | RED: 15, 20 | AMBER: 63, 21 | YELLOW: 62, 22 | GREEN: 60 23 | }; 24 | 25 | class Midi { 26 | init(storeObject, clipHandler = () => {}, scheduleRow = () => {}) { 27 | this.store = storeObject; 28 | this.clipHandler = clipHandler; 29 | this.scheduleRow = scheduleRow; 30 | this.previousClips = null; 31 | this.previouseScheduler = null; 32 | this.previousControllers = {}; 33 | 34 | midi.watchPortNames(this.setControllers); 35 | 36 | storeObject.subscribe(() => { 37 | const { clips, scheduler, controllers } = storeObject.getState(); 38 | if (clips !== this.previousClips 39 | || scheduler !== this.previouseScheduler 40 | || controllers !== this.previousControllers) { 41 | this.previousClips = clips; 42 | this.previouseScheduler = scheduler; 43 | this.previousControllers = controllers; 44 | this.updateControllers(); 45 | } 46 | }); 47 | } 48 | 49 | setControllers = (controllers) => { 50 | const { store } = this; 51 | // remove controllers that were there previously 52 | Object.keys(this.previousControllers).forEach((id) => { 53 | this.previousControllers[id].controller.close(); 54 | store.dispatch(removeController(id)); 55 | }); 56 | 57 | const currentControllers = store.getState().controllers; 58 | controllers.forEach((id, index) => { 59 | if (!currentControllers[id]) { 60 | const controller = midi(id, {}); 61 | controller.on('data', ([type, key, data]) => { 62 | if (id === 'Launch Control') { 63 | // knobs 64 | if (type === 184 && key > 20 && key < 49) { 65 | // const y = Math.floor(key / 20) - 1; 66 | const x = (key % 20) - 1; 67 | const value = data / 127; 68 | switch (x) { 69 | case 0: 70 | return store.dispatch(setRenderparam('pointSize', value * 0.5)); 71 | case 1: 72 | return store.dispatch(setRenderparam('luminanceMin', value)); 73 | case 2: 74 | return store.dispatch(setRenderparam('luminanceMax', value)); 75 | case 3: 76 | return store.dispatch(setRenderparam('r0', value)); 77 | default: 78 | return; 79 | } 80 | } 81 | } else { 82 | const state = this.store.getState(); 83 | const down = data > 0; 84 | const y = Math.floor(key / 16); 85 | const x = key % 16; 86 | if (down) { 87 | const padId = state.controllers[id].pad; 88 | const pad = state.pads[padId]; 89 | 90 | // this controller is not controlling a clip 91 | if (!padId || !pad) { return; } 92 | 93 | if (x < 8 && y < 8) { 94 | const clipId = pad.clips[y][x]; 95 | this.clipHandler(clipId, pad, x, y); 96 | } else if (type === 176 && (key >= 104 || key <= 111)) { 97 | this.mapControllerToPadIndex(id, key - 104); 98 | } else if (type === 144 && (key < 105 || key > 111)) { 99 | this.scheduleRow(pad, y); 100 | } 101 | } 102 | } 103 | }); 104 | // map controller to matching pad at index if possible 105 | const padKeys = Object.keys(store.getState().pads); 106 | const initialPad = index < padKeys.length ? padKeys[index] : padKeys[0]; 107 | store.dispatch(addController(id, controller, initialPad)); 108 | } 109 | }); 110 | } 111 | 112 | mapControllerToPadIndex(controller, padIndex) { 113 | const { pads } = this.store.getState(); 114 | const padId = Object.keys(pads)[padIndex]; 115 | // `padIndex` could be any index even if no pad assigned to it 116 | if (padId) { 117 | this.store.dispatch(mapControllerToPad(controller, padId)); 118 | } 119 | } 120 | 121 | updateControllers = () => { 122 | const { clips, pads, controllers, scheduler } = this.store.getState(); 123 | 124 | Object.keys(controllers).forEach((controllerId, index) => { 125 | const controllerConfig = controllers[controllerId]; 126 | const { controller } = controllerConfig; 127 | const pad = pads[controllerConfig.pad]; 128 | 129 | // write the clip status 130 | for (var y = 0; y < 8; y++) { 131 | for (var x = 0; x < 8; x++) { 132 | const key = y * 16 + x; 133 | if (pad) { 134 | const padClip = pad.clips[y][x]; 135 | const clip = clips[padClip]; 136 | 137 | if (clip) { 138 | const file = isPlaying(scheduler.playing, clip); 139 | 140 | if (scheduler.scheduled[padClip]) { 141 | controller.write([144, key, COLOR_CODES.YELLOW]); 142 | } else if(scheduler.toStop[padClip]) { 143 | controller.write([144, key, COLOR_CODES.RED]); 144 | } else if (file && file.clipId === padClip) { 145 | controller.write([144, key, COLOR_CODES.AMBER]); 146 | } else { 147 | controller.write([144, key, COLOR_CODES.GREEN]); 148 | } 149 | } else { 150 | controller.write([144, key, COLOR_CODES.OFF]); 151 | } 152 | } else { 153 | controller.write([144, key, COLOR_CODES.OFF]); 154 | } 155 | } 156 | } 157 | 158 | // write the pad status 159 | const padKeys = Object.keys(pads); 160 | const activePadId = pad && padKeys.indexOf(controllerConfig.pad); 161 | for (var i = 0; i < 8; i++) { 162 | controller.write([ 163 | 176, 164 | 104 + i, 165 | activePadId === i ? COLOR_CODES.GREEN : i < padKeys.length ? COLOR_CODES.YELLOW : COLOR_CODES.OFF 166 | ]); 167 | } 168 | }); 169 | } 170 | } 171 | 172 | const instance = new Midi(); 173 | export default instance; 174 | -------------------------------------------------------------------------------- /src/clip/clip-editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | CLIP_TYPE_NONE, 5 | CLIP_TYPE_AUDIO_SAMPLE, 6 | CLIP_TYPE_AUDIO_AND_VIDEO, 7 | CLIP_TYPE_VIDEO, 8 | CLIP_TYPES, 9 | AUDIO_BEHAVIOR_TYPES, 10 | changeClipField, 11 | deleteClip 12 | } from '../data/clips'; 13 | import { getAudioFiles, getVideoFiles } from '../data/files'; 14 | 15 | import '../styles/forms.css'; 16 | import './clip-editor.css'; 17 | 18 | class ClipEditor extends Component { 19 | shouldComponentUpdate(newProps){ 20 | return newProps.clip !== this.props.clip 21 | || newProps.tracks !== this.props.tracks; 22 | } 23 | 24 | render() { 25 | const currentType = this.props.clip.type || CLIP_TYPE_NONE; 26 | const { track, tracks } = this.props; 27 | 28 | return ( 29 |
30 | 41 | { this.renderForm() } 42 | 54 | 55 | 56 |
57 | ); 58 | } 59 | 60 | renderForm() { 61 | const { clip, audioFiles, videoFiles } = this.props; 62 | const { behavior, file, videoFile, gain, loop, noFilter, type } = clip; 63 | const hasAudio = type === CLIP_TYPE_AUDIO_AND_VIDEO || type === CLIP_TYPE_AUDIO_SAMPLE; 64 | const hasVideo = type === CLIP_TYPE_AUDIO_AND_VIDEO || type === CLIP_TYPE_VIDEO; 65 | const isVideo = type === CLIP_TYPE_VIDEO; 66 | 67 | return ( 68 |
69 | 80 | 83 | 94 | { hasAudio && ( 95 |
96 | Audio Sample: 97 | 110 |
111 | )} 112 | 113 | { hasVideo && ( 114 |
115 | Video File: 116 | 129 |
130 | )} 131 | 132 | { isVideo && ( 133 | 136 | )} 137 |
138 | ); 139 | } 140 | 141 | changeBehavior = (event) => { 142 | this.props.changeClipField('behavior', event.target.value); 143 | } 144 | 145 | changeType = (event) => { 146 | this.props.changeClipField('type', event.target.value); 147 | } 148 | 149 | changeSchedulable = () => { 150 | this.props.changeClipField('schedulable', !this.props.clip.schedulable); 151 | } 152 | 153 | changeLoop = (event) => { 154 | this.props.changeClipField('loop', event.target.checked); 155 | } 156 | 157 | changeNoFilter = (event) => { 158 | this.props.changeClipField('noFilter', event.target.checked); 159 | } 160 | 161 | changeGain = (event) => { 162 | this.props.changeClipField('gain', parseFloat(event.target.value)); 163 | } 164 | 165 | changeFile = (event) => { 166 | this.props.changeClipField('file', event.target.value); 167 | } 168 | 169 | changeVideoFile = (event) => { 170 | this.props.changeClipField('videoFile', event.target.value); 171 | } 172 | 173 | changeTrack = (event) => { 174 | this.props.changeClipField('track', event.target.value); 175 | } 176 | 177 | deleteClip = (event) => { 178 | event.preventDefault(); 179 | this.props.deleteClip(this.props.clip.id); 180 | } 181 | 182 | } 183 | 184 | const mapStateToProps = (state) => ({ 185 | tracks: state.tracks, 186 | audioFiles: getAudioFiles(state), 187 | videoFiles: getVideoFiles(state) 188 | }); 189 | 190 | const mapDispatchToProps = (dispatch, { clip, padId }) => ({ 191 | changeClipField(field, value) { 192 | return dispatch(changeClipField(clip.id, field, value)); 193 | }, 194 | 195 | deleteClip(id) { 196 | dispatch(deleteClip(id, padId)); 197 | } 198 | }) 199 | 200 | export default connect(mapStateToProps, mapDispatchToProps)(ClipEditor); 201 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.NODE_ENV = 'production'; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | var chalk = require('chalk'); 11 | var fs = require('fs-extra'); 12 | var path = require('path'); 13 | var filesize = require('filesize'); 14 | var gzipSize = require('gzip-size').sync; 15 | var rimrafSync = require('rimraf').sync; 16 | var webpack = require('webpack'); 17 | var config = require('../config/webpack.config.prod'); 18 | var paths = require('../config/paths'); 19 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | var recursive = require('recursive-readdir'); 21 | var stripAnsi = require('strip-ansi'); 22 | 23 | // Warn and crash if required files are missing 24 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 25 | process.exit(1); 26 | } 27 | 28 | // Input: /User/dan/app/build/static/js/main.82be8.js 29 | // Output: /static/js/main.js 30 | function removeFileNameHash(fileName) { 31 | return fileName 32 | .replace(paths.appBuild, '') 33 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 34 | } 35 | 36 | // Input: 1024, 2048 37 | // Output: "(+1 KB)" 38 | function getDifferenceLabel(currentSize, previousSize) { 39 | var FIFTY_KILOBYTES = 1024 * 50; 40 | var difference = currentSize - previousSize; 41 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 42 | if (difference >= FIFTY_KILOBYTES) { 43 | return chalk.red('+' + fileSize); 44 | } else if (difference < FIFTY_KILOBYTES && difference > 0) { 45 | return chalk.yellow('+' + fileSize); 46 | } else if (difference < 0) { 47 | return chalk.green(fileSize); 48 | } else { 49 | return ''; 50 | } 51 | } 52 | 53 | // First, read the current file sizes in build directory. 54 | // This lets us display how much they changed later. 55 | recursive(paths.appBuild, (err, fileNames) => { 56 | var previousSizeMap = (fileNames || []) 57 | .filter(fileName => /\.(js|css)$/.test(fileName)) 58 | .reduce((memo, fileName) => { 59 | var contents = fs.readFileSync(fileName); 60 | var key = removeFileNameHash(fileName); 61 | memo[key] = gzipSize(contents); 62 | return memo; 63 | }, {}); 64 | 65 | // Remove all content but keep the directory so that 66 | // if you're in it, you don't end up in Trash 67 | rimrafSync(paths.appBuild + '/*'); 68 | 69 | // Start the webpack build 70 | build(previousSizeMap); 71 | 72 | // Merge with the public folder 73 | copyPublicFolder(); 74 | }); 75 | 76 | // Print a detailed summary of build files. 77 | function printFileSizes(stats, previousSizeMap) { 78 | var assets = stats.toJson().assets 79 | .filter(asset => /\.(js|css)$/.test(asset.name)) 80 | .map(asset => { 81 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 82 | var size = gzipSize(fileContents); 83 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 84 | var difference = getDifferenceLabel(size, previousSize); 85 | return { 86 | folder: path.join('build', path.dirname(asset.name)), 87 | name: path.basename(asset.name), 88 | size: size, 89 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') 90 | }; 91 | }); 92 | assets.sort((a, b) => b.size - a.size); 93 | var longestSizeLabelLength = Math.max.apply(null, 94 | assets.map(a => stripAnsi(a.sizeLabel).length) 95 | ); 96 | assets.forEach(asset => { 97 | var sizeLabel = asset.sizeLabel; 98 | var sizeLength = stripAnsi(sizeLabel).length; 99 | if (sizeLength < longestSizeLabelLength) { 100 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 101 | sizeLabel += rightPadding; 102 | } 103 | console.log( 104 | ' ' + sizeLabel + 105 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 106 | ); 107 | }); 108 | } 109 | 110 | // Print out errors 111 | function printErrors(summary, errors) { 112 | console.log(chalk.red(summary)); 113 | console.log(); 114 | errors.forEach(err => { 115 | console.log(err.message || err); 116 | console.log(); 117 | }); 118 | } 119 | 120 | // Create the production build and print the deployment instructions. 121 | function build(previousSizeMap) { 122 | console.log('Creating an optimized production build...'); 123 | webpack(config).run((err, stats) => { 124 | if (err) { 125 | printErrors('Failed to compile.', [err]); 126 | process.exit(1); 127 | } 128 | 129 | if (stats.compilation.errors.length) { 130 | printErrors('Failed to compile.', stats.compilation.errors); 131 | process.exit(1); 132 | } 133 | 134 | console.log(chalk.green('Compiled successfully.')); 135 | console.log(); 136 | 137 | console.log('File sizes after gzip:'); 138 | console.log(); 139 | printFileSizes(stats, previousSizeMap); 140 | console.log(); 141 | 142 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 143 | var homepagePath = require(paths.appPackageJson).homepage; 144 | var publicPath = config.output.publicPath; 145 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 146 | // "homepage": "http://user.github.io/project" 147 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 148 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 149 | console.log(); 150 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 151 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); 152 | console.log(); 153 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); 154 | console.log(); 155 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); 156 | console.log(); 157 | console.log(' ' + chalk.dim('// ...')); 158 | console.log(' ' + chalk.yellow('"scripts"') + ': {'); 159 | console.log(' ' + chalk.dim('// ...')); 160 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"gh-pages -d build"')); 161 | console.log(' }'); 162 | console.log(); 163 | console.log('Then run:'); 164 | console.log(); 165 | console.log(' ' + chalk.cyan('npm') + ' run deploy'); 166 | console.log(); 167 | } else if (publicPath !== '/') { 168 | // "homepage": "http://mywebsite.com/project" 169 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 170 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 171 | console.log(); 172 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 173 | console.log(); 174 | } else { 175 | // no homepage or "homepage": "http://mywebsite.com" 176 | console.log('The project was built assuming it is hosted at the server root.'); 177 | if (homepagePath) { 178 | // "homepage": "http://mywebsite.com" 179 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 180 | console.log(); 181 | } else { 182 | // no homepage 183 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); 184 | console.log('For example, add this to build it for GitHub Pages:') 185 | console.log(); 186 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); 187 | console.log(); 188 | } 189 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 190 | console.log('You may also serve it locally with a static server:') 191 | console.log(); 192 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); 193 | console.log(' ' + chalk.cyan('pushstate-server') + ' build'); 194 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); 195 | console.log(); 196 | } 197 | }); 198 | } 199 | 200 | function copyPublicFolder() { 201 | fs.copySync(paths.appPublic, paths.appBuild, { 202 | dereference: true, 203 | filter: file => file !== paths.appHtml 204 | }); 205 | } 206 | -------------------------------------------------------------------------------- /public/initial/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "gain": 1, 4 | "bpm": 100 5 | }, 6 | "tracks": { 7 | "master": { 8 | "name": "master", 9 | "id": "master", 10 | "gain": 1, 11 | "filters": [ 12 | "239786428374t928374628934728347" 13 | ] 14 | }, 15 | "secondChannel": { 16 | "name": "master2", 17 | "id": "secondChannel", 18 | "gain": 1, 19 | "filters": [] 20 | } 21 | }, 22 | "filters": { 23 | "239786428374t928374628934728347": { 24 | "type": "lowpass" 25 | } 26 | }, 27 | "pads": { 28 | "pad1": { 29 | "id": "pad1", 30 | "clips": [ 31 | [ 32 | "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3", 33 | "f954f9b6-4055-4533-a3d7-416ee1441bba", 34 | "473ef6f9-16e8-48e1-a809-54693a17a509", 35 | "a18d0496-dd87-4a7c-ac95-4d76b4728595", 36 | null, 37 | null, 38 | null, 39 | null 40 | ], 41 | [ 42 | "2316526c-ec07-4153-a47a-d831b2aaf18e", 43 | "762e04a0-b651-4560-bad1-31011b7b27e5", 44 | "ccda5e09-62b6-40af-be94-e10077bf6e59", 45 | "e39f4804-4a40-42f7-bac5-eade1ba47b96", 46 | null, 47 | null, 48 | null, 49 | null 50 | ], 51 | [ 52 | null, 53 | null, 54 | null, 55 | null, 56 | null, 57 | null, 58 | null, 59 | null 60 | ], 61 | [ 62 | null, 63 | null, 64 | null, 65 | null, 66 | null, 67 | null, 68 | null, 69 | null 70 | ], 71 | [ 72 | null, 73 | null, 74 | null, 75 | null, 76 | null, 77 | null, 78 | null, 79 | null 80 | ], 81 | [ 82 | null, 83 | null, 84 | null, 85 | null, 86 | null, 87 | null, 88 | null, 89 | null 90 | ], 91 | [ 92 | null, 93 | null, 94 | null, 95 | null, 96 | null, 97 | null, 98 | null, 99 | null 100 | ], 101 | [ 102 | null, 103 | null, 104 | null, 105 | null, 106 | null, 107 | null, 108 | null, 109 | null 110 | ] 111 | ], 112 | "selectedClipId": "e39f4804-4a40-42f7-bac5-eade1ba47b96" 113 | }, 114 | "pad2": { 115 | "id": "pad2", 116 | "clips": [ 117 | [ 118 | null, 119 | null, 120 | null, 121 | null, 122 | null, 123 | null, 124 | null, 125 | null 126 | ], 127 | [ 128 | null, 129 | null, 130 | null, 131 | null, 132 | null, 133 | null, 134 | null, 135 | null 136 | ], 137 | [ 138 | null, 139 | null, 140 | null, 141 | null, 142 | null, 143 | null, 144 | null, 145 | null 146 | ], 147 | [ 148 | "e39f4804-4a40-42f7-bac5-eade1ba47b910", 149 | "e39f4804-4a40-42f7-bac5-eade1ba47b911", 150 | null, 151 | null, 152 | null, 153 | null, 154 | null, 155 | null 156 | ], 157 | [ 158 | null, 159 | null, 160 | null, 161 | null, 162 | null, 163 | null, 164 | null, 165 | null 166 | ], 167 | [ 168 | null, 169 | null, 170 | null, 171 | null, 172 | null, 173 | null, 174 | null, 175 | null 176 | ], 177 | [ 178 | null, 179 | null, 180 | null, 181 | null, 182 | null, 183 | null, 184 | null, 185 | null 186 | ], 187 | [ 188 | null, 189 | null, 190 | null, 191 | null, 192 | null, 193 | null, 194 | null, 195 | null 196 | ] 197 | ], 198 | "selectedClipId": "2316526c-ec07-4153-a47a-d831b2aaf18e" 199 | } 200 | }, 201 | "clips": { 202 | "2316526c-ec07-4153-a47a-d831b2aaf18e": { 203 | "id": "2316526c-ec07-4153-a47a-d831b2aaf18e", 204 | "type": "audiosample", 205 | "behavior": "single", 206 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21685", 207 | "track": "master", 208 | "gain": 1, 209 | "loop": false 210 | }, 211 | "762e04a0-b651-4560-bad1-31011b7b27e5": { 212 | "id": "762e04a0-b651-4560-bad1-31011b7b27e5", 213 | "type": "audiosample", 214 | "behavior": "single", 215 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21686", 216 | "track": "master", 217 | "gain": 1, 218 | "loop": false 219 | }, 220 | "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3": { 221 | "gain": 1, 222 | "behavior": "single", 223 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21681", 224 | "loop": false, 225 | "track": "master", 226 | "id": "63dd984a-c6b8-4d5c-bcd1-4cdab36055f3", 227 | "type": "audiosample" 228 | }, 229 | "f954f9b6-4055-4533-a3d7-416ee1441bba": { 230 | "gain": 1, 231 | "behavior": "single", 232 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21682", 233 | "loop": false, 234 | "track": "master", 235 | "id": "f954f9b6-4055-4533-a3d7-416ee1441bba", 236 | "type": "audiosample" 237 | }, 238 | "473ef6f9-16e8-48e1-a809-54693a17a509": { 239 | "gain": 1, 240 | "behavior": "single", 241 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21683", 242 | "loop": false, 243 | "track": "master", 244 | "id": "473ef6f9-16e8-48e1-a809-54693a17a509", 245 | "type": "audiosample" 246 | }, 247 | "a18d0496-dd87-4a7c-ac95-4d76b4728595": { 248 | "gain": 1, 249 | "behavior": "single", 250 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21684", 251 | "loop": false, 252 | "track": "master", 253 | "id": "a18d0496-dd87-4a7c-ac95-4d76b4728595", 254 | "type": "audiosample" 255 | }, 256 | "ccda5e09-62b6-40af-be94-e10077bf6e59": { 257 | "gain": 1, 258 | "behavior": "single", 259 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21687", 260 | "loop": false, 261 | "track": "master", 262 | "id": "ccda5e09-62b6-40af-be94-e10077bf6e59", 263 | "type": "audiosample" 264 | }, 265 | "e39f4804-4a40-42f7-bac5-eade1ba47b96": { 266 | "gain": 1, 267 | "behavior": "single", 268 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec21688", 269 | "loop": false, 270 | "track": "master", 271 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b96", 272 | "type": "audiosample" 273 | }, 274 | "e39f4804-4a40-42f7-bac5-eade1ba47b910": { 275 | "gain": 1, 276 | "behavior": "single", 277 | "file": "2a86e9ee-b409-4e2b-91e0-bcc051ec2168", 278 | "loop": false, 279 | "track": "master", 280 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b910", 281 | "type": "audiosample" 282 | }, 283 | "e39f4804-4a40-42f7-bac5-eade1ba47b911": { 284 | "gain": 1, 285 | "behavior": "single", 286 | "file": "36c1262c-608c-41db-a12c-7a2fad9d4c14", 287 | "loop": false, 288 | "track": "master", 289 | "id": "e39f4804-4a40-42f7-bac5-eade1ba47b911", 290 | "type": "audiosample" 291 | } 292 | }, 293 | "files": { 294 | "2a86e9ee-b409-4e2b-91e0-bcc051ec2168": { 295 | "location": "mysound.mp3", 296 | "name": "mysound" 297 | }, 298 | "36c1262c-608c-41db-a12c-7a2fad9d4c14": { 299 | "location": "mysound2.mp3", 300 | "name": "mysound2" 301 | }, 302 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21681": { 303 | "location": "1.mp3", 304 | "name": "git1" 305 | }, 306 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21682": { 307 | "location": "2.mp3", 308 | "name": "git2" 309 | }, 310 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21683": { 311 | "location": "3.mp3", 312 | "name": "kick" 313 | }, 314 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21684": { 315 | "location": "4.mp3", 316 | "name": "clap" 317 | }, 318 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21685": { 319 | "location": "5.mp3", 320 | "name": "bell1" 321 | }, 322 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21686": { 323 | "location": "6.mp3", 324 | "name": "bell2" 325 | }, 326 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21687": { 327 | "location": "7.mp3", 328 | "name": "bell3" 329 | }, 330 | "2a86e9ee-b409-4e2b-91e0-bcc051ec21688": { 331 | "location": "8.mp3", 332 | "name": "bell4" 333 | } 334 | }, 335 | "scheduler": { 336 | "scheduled": {}, 337 | "playing": {}, 338 | "toStop": {} 339 | } 340 | } -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var findCacheDir = require('find-cache-dir'); 5 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 7 | var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 8 | var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 9 | var getClientEnvironment = require('./env'); 10 | var paths = require('./paths'); 11 | 12 | // Webpack uses `publicPath` to determine where the app is being served from. 13 | // In development, we always serve from the root. This makes config easier. 14 | var publicPath = '/'; 15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 18 | var publicUrl = ''; 19 | // Get environment variables to inject into our app. 20 | var env = getClientEnvironment(publicUrl); 21 | 22 | // This is the development configuration. 23 | // It is focused on developer experience and fast rebuilds. 24 | // The production configuration is different and lives in a separate file. 25 | module.exports = { 26 | target: 'electron-renderer', 27 | // This makes the bundle appear split into separate modules in the devtools. 28 | // We don't use source maps here because they can be confusing: 29 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875 30 | // You may want 'cheap-module-source-map' instead if you prefer source maps. 31 | devtool: 'eval', 32 | // These are the "entry points" to our application. 33 | // This means they will be the "root" imports that are included in JS bundle. 34 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 35 | entry: [ 36 | // Include an alternative client for WebpackDevServer. A client's job is to 37 | // connect to WebpackDevServer by a socket and get notified about changes. 38 | // When you save a file, the client will either apply hot updates (in case 39 | // of CSS changes), or refresh the page (in case of JS changes). When you 40 | // make a syntax error, this client will display a syntax error overlay. 41 | // Note: instead of the default WebpackDevServer client, we use a custom one 42 | // to bring better experience for Create React App users. You can replace 43 | // the line below with these two lines if you prefer the stock client: 44 | // require.resolve('webpack-dev-server/client') + '?/', 45 | // require.resolve('webpack/hot/dev-server'), 46 | require.resolve('react-dev-utils/webpackHotDevClient'), 47 | // We ship a few polyfills by default: 48 | require.resolve('./polyfills'), 49 | // Finally, this is your app's code: 50 | paths.appIndexJs 51 | // We include the app code last so that if there is a runtime error during 52 | // initialization, it doesn't blow up the WebpackDevServer client, and 53 | // changing JS code would still trigger a refresh. 54 | ], 55 | output: { 56 | // Next line is not used in dev but WebpackDevServer crashes without it: 57 | path: paths.appBuild, 58 | // Add /* filename */ comments to generated require()s in the output. 59 | pathinfo: true, 60 | // This does not produce a real file. It's just the virtual path that is 61 | // served by WebpackDevServer in development. This is the JS bundle 62 | // containing code from all our entry points, and the Webpack runtime. 63 | filename: 'static/js/bundle.js', 64 | // This is the URL that app is served from. We use "/" in development. 65 | publicPath: publicPath 66 | }, 67 | resolve: { 68 | // This allows you to set a fallback for where Webpack should look for modules. 69 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 70 | // We use `fallback` instead of `root` because we want `node_modules` to "win" 71 | // if there any conflicts. This matches Node resolution mechanism. 72 | // https://github.com/facebookincubator/create-react-app/issues/253 73 | fallback: paths.nodePaths, 74 | // These are the reasonable defaults supported by the Node ecosystem. 75 | // We also include JSX as a common component filename extension to support 76 | // some tools, although we do not recommend using it, see: 77 | // https://github.com/facebookincubator/create-react-app/issues/290 78 | extensions: ['.js', '.json', '.jsx', ''], 79 | alias: { 80 | // Support React Native Web 81 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 82 | 'react-native': 'react-native-web' 83 | } 84 | }, 85 | 86 | module: { 87 | // First, run the linter. 88 | // It's important to do this before Babel processes the JS. 89 | preLoaders: [ 90 | { 91 | test: /\.(js|jsx)$/, 92 | loader: 'eslint', 93 | include: paths.appSrc, 94 | } 95 | ], 96 | loaders: [ 97 | // Process JS with Babel. 98 | { 99 | test: /\.(js|jsx)$/, 100 | include: paths.appSrc, 101 | loader: 'babel', 102 | query: { 103 | 104 | // This is a feature of `babel-loader` for webpack (not Babel itself). 105 | // It enables caching results in ./node_modules/.cache/react-scripts/ 106 | // directory for faster rebuilds. We use findCacheDir() because of: 107 | // https://github.com/facebookincubator/create-react-app/issues/483 108 | cacheDirectory: findCacheDir({ 109 | name: 'react-scripts' 110 | }) 111 | } 112 | }, 113 | // "postcss" loader applies autoprefixer to our CSS. 114 | // "css" loader resolves paths in CSS and adds assets as dependencies. 115 | // "style" loader turns CSS into JS modules that inject