123

Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              @use cssnext;
@use postcss-nested;

@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i);

:root {
  --color-1: #263238;
  --color-2: #0091EA;
}

*,
*::after,
*::before {
  box-sizing: inherit;
}

html {
  box-sizing: border-box;
  font-size: 16px;
}

body {
  background: #fff;
  line-height: 1;
  font-family: sans-serif;
  font-size: 1rem;
  color: #000;
}

.ad-Root {
  width: 100%;
  height: 100vh;
  font-family: "Roboto", sans-serif;
}

.ad-Composer {
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

.ad-Overlay {
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  background: color(var(--color-2) a(.25));
  transition: opacity .2s, visibility .2s;
  font-size: 1.6rem;
  font-weight: 500;
  color: #fff;

  &.is-over {
    opacity: 1;
    visibility: visible;
  }
}
  
.ad-Head {
  padding: 1rem;
  display: flex;
  align-items: center;
  background: var(--color-1);
  
  &-actions {
    flex: 1;
  }
  &-settings {
    display: flex;
  }
}

.ad-Setting {
  display: block;
  padding: .75rem;
  width: 4rem;
  background: color(var(--color-1) l(+5%));
  border-radius: 2px;
  
  & + & {
    margin-left: .25rem;
  }
  
  &-title {
    margin-bottom: .25rem;
    text-align: center;
    text-transform: uppercase;
    font-size: .7rem;
    color: color(var(--color-1) l(+50%));
  }
  &-value {}
    &-input {
      width: 100%;
      background: none;
      border: none;
      border-radius: 0;
      text-align: center;
      font-family: inherit;
      font-size: .75rem;
      color: #fff;
    }
}

.ad-Workspace {
  flex: 1;
  width: 100%;
  overflow: auto;
  
  &-frame {
    display: flex;
  }
  &-content {
    flex: 1;
    position: relative;
    overflow: hidden;
  }
}

.ad-Layer {
  z-index: 1000;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  pointer-events: none;
}

.ad-Scale {
  z-index: 10;
  position: relative;
  height: 2rem;
  display: flex;
  align-items: center;
  cursor: pointer;
  background: color(var(--color-1) l(-5%));
  
  &-unit {
    flex: 1;
    padding: 0 .5rem;
    text-align: right;
    font-size: .7rem;
    color: #fff;
  }
}

.ad-Frequencies {
  z-index: 30;
  position: relative;
  width: 5rem;
  
  &-patch {
    width: 100%;
    height: 2rem;
    background: color(var(--color-1) l(-5%));
  }
}

.ad-Frequency {
  display: flex;
  align-items: center;
  padding: 0 .5rem;
  background: color(var(--color-1) l(+80%));
  border-top: 1px solid color(var(--color-1) l(+65%));
  font-size: .75rem;
  color: var(--color-1);
  
  &--sharp {
    background: color(var(--color-1) l(-5%));
    border-right: none;
    color: #fff;
  }
  &--octave {
    border-top-color: color(var(--color-1) l(+55%));
  }
}

.ad-Marker {
  z-index: 20;
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--color-2);
  
  &::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    transform: translateX(-50%);
    border: 10px solid transparent;
    border-top-color: var(--color-2);
  }
}

.ad-Notes {
  position: relative;
}

.ad-DraggableNote {
  position: absolute;

  &-note {
    height: 100%;
  }
  &-grip {
    z-index: 5;
    position: absolute;
    top: 0;
    bottom: 0;
    width: 10%;
    cursor: ew-resize;
    transition: opacity .2s;
    opacity: 0;
    
    &::before {
      content: "";
      position: absolute;
      top: .15rem;
      bottom: .15rem;
      width: .5rem;
      background: color(var(--color-2) l(-10%));
      border-radius: 2px;
    }
    
    &--left {
      left: 0;
      
      &::before {
        left: .15rem;
      }
    }
    &--right {
      right: 0;
      
      &::before {
        right: .15rem;
      }
    }
    &--show {
      opacity: 1;
    }
  }
  &:hover &-grip {
    opacity: 1;
  }
}

.ad-Note {
  overflow: hidden;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  padding: 0 .5rem;
  background: var(--color-2);
  border-radius: 2px;
  box-shadow: 0 2px 4px color(var(--color-1) a(.25));
  cursor: move;
  font-size: .7rem;
  color: #fff;
}

.ad-Grid {
  fill: none;
  stroke-width: 1px;
  background: var(--color-1);
  
  &-line {
    stroke: color(var(--color-1) l(-3%));
    
    &--octave {
      stroke: color(var(--color-1) l(-6%));
    }
  }
}

.ad-Button {
  background: none;
  border: none;
  cursor: pointer;
  outline: 0;
  transition: background .2s, border .2s, color .2s;
  font-family: inherit;
  
  &New {
    flex: 1;
    padding: 1rem;
    border-radius: 2px;
    background: color(var(--color-2) l(+15%));
    transition: background .2s;
    
    & + & {
      margin-left: .5rem;
    }
    
    &:focus,
    &:hover {
      background: color(var(--color-2) l(+20%));
    }

    &-icon {
      font-size: 1.6rem;
      color: #fff;
    }
    &-title {
      line-height: 1.2;
      font-size: .8rem;
      font-weight: 500;
      color: #fff;
    }
    &-subtitle {
      font-size: .7rem;
      color: color(var(--color-2) l(+40%));
    }
  }
  
  &--play,
  &--pause {
    width: 3.5rem;
    height: 3.5rem;
    border: 2px solid var(--color-2);
    border-radius: 50%;
    font-size: 1.5rem;
  }
  &--play {
    color: var(--color-2);
    
    &:focus,
    &:hover {
      background: var(--color-2);
      color: #fff;
    }
  }
  &--pause {
    background: var(--color-2);
    color: #fff;
    
    &:focus,
    &:hover {
      background: color(var(--color-2) l(-5%));
      border-color: color(var(--color-2) l(-5%));
    }
  }
  &--stop {
    margin-left: .5rem;
    width: 3rem;
    height: 2.25rem;
    border: 2px solid color(var(--color-1) l(+30%));
    border-radius: 2px;
    color: color(var(--color-1) l(+30%));
    font-size: 1.25rem;
    
    &:focus,
    &:hover {
      background: color(var(--color-1) l(+30%));
      color: #fff;
    }
  }
  &--disabled {
    &,
    &:focus,
    &:hover {
      background: none;
      border-color: color(var(--color-1) l(+15%));
      cursor: not-allowed;
      color: color(var(--color-1) l(+15%));
    }
  }
    &-content {
      display: flex;
      align-items: center;
      justify-content: center;
    }
}

.ad-Modal {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  z-index: 100;
  position: fixed;
  top: 0;
  left: 0;
  background: color(var(--color-1) a(.75));

  &-content {
    overflow: hidden;
    width: 100%;
    max-width: 36rem;
    background: var(--color-2);
    border-radius: 2px;
    box-shadow: 0 4px 8px color(#000 a(.25));
  }
    &-head {
      padding: 2rem;
      text-align: center;
      font-size: 1.1rem;
      font-weight: 500;
      color: #fff;
    }
    &-body {
      padding: 2rem;
      padding-top: 0;
    }
      &-intro {
        text-align: center;
        line-height: 1.4;
        font-size: .95rem;
        color: #fff;
      }
      &-features {
        padding: 2rem 1rem 0;
      }
        &-feature {
          display: flex;
          align-items: center;
          justify-content: center;

          & + & {
            margin-top: .5rem;
          }
        }
          &-action {
            flex: 1;
            padding: .25rem .5rem;
            margin-right: .5rem;
            background: color(var(--color-2) l(-8%));
            border-radius: 2px;
            text-align: right;
            text-transform: uppercase;
            font-size: .7rem;
            color: #fff;
          }
          &-hint {
            flex: 1;
            font-size: .95rem;
            color: #fff;
          }
    
    &-foot {
      padding: 2rem;
      padding-bottom: 1rem;
      background: color(var(--color-2) l(+8%));
    }
      &-title {
        text-align: center;
        text-transform: uppercase;
        font-size: 1rem;
        color: #fff;
      }
      &-examples {
        padding: 1rem 0;
        display: flex;
        justify-content: center;
      }
      &-more {
        padding-top: 1rem;
        border-top: 1px solid color(var(--color-2) l(+15%));
        text-align: center;
        font-size: .7rem;
        color: #fff;

        & a {
          color: inherit;
        }
      }
}

            
          
!
            
              const { Component, PureComponent, PropTypes } = React
const { render, findDOMNode } = ReactDOM
const { DragDropContext, DragSource, DragLayer, DropTarget } = ReactDnD
const HTML5Backend = ReactDnDHTML5Backend
const cx = classNames

const types = {
  note: PropTypes.shape({
    start: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    frequency: PropTypes.number.isRequired,
  }),
}

const utils = {
  sortedTypes(noteTypes) {
    return Object.keys(noteTypes).sort((a, b) => noteTypes[b] - noteTypes[a])
  },
  isSharp(noteType) {
    return noteType.toLowerCase().endsWith("#")
  },
  clamp(n, min, max) {
    return Math.min(max, Math.max(min, n))
  },
}

const ItemTypes = {
  NOTE: "NOTE",
  NOTE_GRIP_LEFT: "NOTE_GRIP_LEFT",
  NOTE_GRIP_RIGHT: "NOTE_GRIP_RIGHT",
}

const FrequencyTypes = {
  "B7": 3951.07,
  "A7#": 3729.31,
  "A7": 3520.00,
  "G7#": 3322.44,
  "G7": 3135.96,
  "F7#": 2959.96,
  "F7": 2793.83,
  "E7": 2637.02,
  "D7#": 2489.02,
  "D7": 2349.32,
  "C7#": 2217.46,
  "C7": 2093.00,

  "B6": 1975.53,
  "A6#": 1864.66,
  "A6": 1760.00,
  "G6#": 1661.22,
  "G6": 1567.98,
  "F6#": 1479.98,
  "F6": 1396.91,
  "E6": 1318.51,
  "D6#": 1244.51,
  "D6": 1174.66,
  "C6#": 1108.73,
  "C6": 1046.50,

  "B5": 987.767,
  "A5#": 932.328,
  "A5": 880.000,
  "G5#": 830.609,
  "G5": 783.991,
  "F5#": 739.989,
  "F5": 698.456,
  "E5": 659.255,
  "D5#": 622.254,
  "D5": 587.330,
  "C5#": 554.365,
  "C5": 523.251,

  "B4": 493.883,
  "A4#": 466.164,
  "A4": 440.000,
  "G4#": 415.305,
  "G4": 391.995,
  "F4#": 369.994,
  "F4": 349.228,
  "E4": 329.628,
  "D4#": 311.127,
  "D4": 293.665,
  "C4#": 277.183,
  "C4": 261.626,

  "B3": 246.942,
  "A3#": 233.082,
  "A3": 220.000,
  "G3#": 207.652,
  "G3": 195.998,
  "F3#": 184.997,
  "F3": 174.614,
  "E3": 164.814,
  "D3#": 155.563,
  "D3": 146.832,
  "C3#": 138.591,
  "C3": 130.813,

  "B2": 123.471,
  "A2#": 116.541,
  "A2": 110.000,
  "G2#": 103.826,
  "G2": 97.9989,
  "F2#": 92.4986,
  "F2": 87.3071,
  "E2": 82.4069,
  "D2#": 77.7817,
  "D2": 73.4162,
  "C2#": 69.2957,
  "C2": 65.4064,
}

const sortedFrequencyTypes = utils.sortedTypes(FrequencyTypes)

class Root extends PureComponent {
  constructor(props) {
    super(props)
    
    this.id = 0
    this.timerId = null
    this.nodes = []
  }
  
  state = {
    ctx: new AudioContext(),
    buffer: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/80862/piano2.wav",
    bpm: 100,
    bar: 4,
    beat: 4,
    baseFrequency: "C4",
    isPlaying: false,
    isLoaded: false,
    visible: 16,
    length: 16,
    frequencyHeight: 24,
    drawLength: 4,
    scrollTop: 0,
    scrollLeft: 0,
    start: 0,
    elapsed: 0,
    showModal: true,
    notes: [],
  };

  componentDidMount() {
    this.workspace.addEventListener("scroll", this.handleWorkspaceScroll)    
    this.loadBuffer()
  }

  componentWillUnmount() {
    this.workspace.removeEventListener("scroll", this.handleWorkspaceScroll)
  }

  handleWorkspaceScroll = () => this.setState({
    scrollTop: this.workspace.scrollTop,
    scrollLeft: this.workspace.scrollLeft,
  });

  scrollTo = (note) => {
    const { frequencyHeight } = this.state
    const scaleHeight = 32
    const height = this.workspace.getBoundingClientRect().height - scaleHeight
    const top = sortedFrequencyTypes.indexOf(note.frequency) * frequencyHeight
    const scrollTop = utils.clamp(
      frequencyHeight + top - (height / 2),
      0,
      this.workspace.scrollHeight - height,
    )
    
    this.setScrollTop(scrollTop)
  };

  decode = (data) => this.state.ctx.decodeAudioData(data).then((buffer) => this.setState({ buffer, isLoaded: true }));

  loadBuffer = () => {
    if (!this.state.isLoaded) {
      fetch(this.state.buffer)
        .then((response) => response.arrayBuffer())
        .then((response) => this.decode(response))
    }
  };

  setBuffer = (file) => {
    if (file.type.startsWith("audio")) {
      this.setState({ isLoaded: false }, () => {
        const fileReader = new FileReader()

        fileReader.onload = (e) => this.decode(e.target.result)
        fileReader.readAsArrayBuffer(file)
      })
    }
  };

  playNote = (note, start) => {
    const {
      ctx,
      bpm,
      beat,
      baseFrequency,
      buffer,
      isLoaded,
    } = this.state
    
    if (isLoaded) {
      const noteTime = 60.0 / bpm / beat
      const noteLength = note.length * noteTime
      const stop = start + noteLength
      const node = ctx.createBufferSource()

      node.buffer = buffer
      node.playbackRate.value = FrequencyTypes[note.frequency] / FrequencyTypes[baseFrequency]
      node.start(start)
      node.stop(stop)
      node.connect(ctx.destination)

      this.nodes.push(node)
    }
  };

  schedule = (start, n, time) => {
    const {
      ctx,
      bpm,
      beat,
      length,
      elapsed,
      notes,
    } = this.state

    const noteTime = 60.0 / bpm / beat
    const currentTime = ctx.currentTime - start

    while (time < currentTime) {
      notes.forEach((note) => note.start === n && this.playNote(note, start + time))

      n += 1
      time += noteTime
    }
    
    // loop
    if (n > length) {
      n = 0
    }

    // update elapsed time
    if (elapsed !== n) {
      this.setState({ elapsed: n })
    }

    this.timerId = requestAnimationFrame(() => this.schedule(start, n, time))
  };

  cancel = () => {
    cancelAnimationFrame(this.timerId)
    
    this.nodes.forEach((node) => node.stop())
    this.timerId = null
    this.nodes = []
  };

  play = () => {
    const {
      ctx,
      start,
    } = this.state

    this.setState({ isPlaying: true }, () => this.schedule(ctx.currentTime, start, 0.0))
  };

  pause = (start) => this.setState(({ elapsed }) => {
    const time = typeof start !== "undefined" ? start : elapsed

    return {
      isPlaying: false,
      elapsed: time,
      start: time,
    }
  }, this.cancel);

  stop = () => this.pause(0);

  setStart = (start) => this.setState({
    elapsed: start,
    start,
  });

  createNote = (start, length, frequency) => this.setState(({ notes }) => ({
    notes: [
      ...notes,
      {
        id: ++this.id,
        start,
        length,
        frequency,
      },
    ],
  }));

  deleteNote = (id) => this.setState(({ notes }) => ({
    notes: notes.filter((note) => note.id !== id),
  }));

  setNoteStart = (id, start) => this.setState(({ notes }) => ({
    notes: notes.map((note) => note.id !== id ? note : { ...note, start }),
  }));

  setNoteLength = (id, length) => this.setState(({ notes }) => ({
    notes: notes.map((note) => note.id !== id ? note : { ...note, length }),
  }));

  setNoteFrequency = (id, frequency) => this.setState(({ notes }) => ({
    notes: notes.map((note) => note.id !== id ? note : { ...note, frequency }),
  }));

  setBpm = (bpm) => this.setState({ bpm });
  
  setBar = (bar) => this.setState({ bar });
  
  setBeat = (beat) => this.setState({ beat });
  
  setVisible = (visible) => this.setState({ visible });
  
  setLength = (length) => this.setState({ length });

  setDrawLength = (drawLength) => this.setState({ drawLength });

  setScrollTop = (scrollTop) => this.setState(
    { scrollTop },
    () => this.workspace.scrollTop = scrollTop,
  );

  setScrollLeft = (scrollLeft) => this.setState(
    { scrollLeft },
    () => this.workspace.scrollLeft = scrollLeft,
  );

  getWorkspaceRef = (node) => this.workspace = node;

  loadState = (file) => {
    fetch(file)
      .then((response) => response.json())
      .then((response) => this.setState(response, () => {
        const { notes } = this.state
        const firstNote = notes[0]
        const lastNote = notes[notes.length - 1]

        this.id = lastNote.id
        this.closeModal()
        this.scrollTo(firstNote)
        this.loadBuffer()
      }))
  };

  closeModal = () => this.setState({ showModal: false });

  render() {
    const {
      isPlaying,
      isLoaded,
      bpm,
      bar,
      beat,
      visible,
      length,
      frequencyHeight,
      drawLength,
      scrollTop,
      scrollLeft,
      elapsed,
      showModal,
      notes,
    } = this.state
    
    return (
      <div className="ad-Root">
        <Composer
          play={ this.play }
          pause={ this.pause }
          stop={ this.stop }
          setBuffer={ this.setBuffer }
          setStart={ this.setStart }
          createNote={ this.createNote }
          deleteNote={ this.deleteNote }
          setNoteStart={ this.setNoteStart }
          setNoteLength={ this.setNoteLength }
          setNoteFrequency={ this.setNoteFrequency }
          setBpm={ this.setBpm }
          setBar={ this.setBar }
          setBeat={ this.setBeat }
          setVisible={ this.setVisible }
          setLength={ this.setLength }
          setDrawLength={ this.setDrawLength }
          getWorkspaceRef={ this.getWorkspaceRef }
          isPlaying={ isPlaying }
          isLoaded={ isLoaded }
          bpm={ bpm }
          bar={ bar }
          beat={ beat }
          visible={ visible }
          length={ length }
          frequencyHeight={ frequencyHeight }
          drawLength={ drawLength }
          scrollTop={ scrollTop }
          scrollLeft={ scrollLeft }
          elapsed={ elapsed }
          notes={ notes } />
        
        <Modal
          loadState={ this.loadState }
          closeModal={ this.closeModal }
          isOpened={ showModal } />
      </div>
    )
  }
}

@DragDropContext(HTML5Backend)
@DropTarget(
  HTML5Backend.NativeTypes.FILE,
  {
    drop(props, monitor, component) {
      const { setBuffer } = props
      const { files } = monitor.getItem()
      
      setBuffer(files[0])
    },
  },
  (connect, monitor) => ({
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
  }),
)
class Composer extends PureComponent {
  static propTypes = {
    connectDropTarget: PropTypes.func.isRequired,
    play: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    stop: PropTypes.func.isRequired,
    setBuffer: PropTypes.func.isRequired,
    setStart: PropTypes.func.isRequired,
    createNote: PropTypes.func.isRequired,
    deleteNote: PropTypes.func.isRequired,
    setNoteStart: PropTypes.func.isRequired,
    setNoteLength: PropTypes.func.isRequired,
    setNoteFrequency: PropTypes.func.isRequired,
    setBpm: PropTypes.func.isRequired,
    setBar: PropTypes.func.isRequired,
    setBeat: PropTypes.func.isRequired,
    setVisible: PropTypes.func.isRequired,
    setLength: PropTypes.func.isRequired,
    setDrawLength: PropTypes.func.isRequired,
    isOver: PropTypes.bool.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    isLoaded: PropTypes.bool.isRequired,
    bpm: PropTypes.number.isRequired,
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    frequencyHeight: PropTypes.number.isRequired,
    drawLength: PropTypes.number.isRequired,
    setScrollTop: PropTypes.number.isRequired,
    setScrollLeft: PropTypes.number.isRequired,
    elapsed: PropTypes.number.isRequired,
    notes: PropTypes.arrayOf(types.note).isRequired,
  };

  render() {
    const {
      connectDropTarget,
      play,
      pause,
      stop,
      setStart,
      createNote,
      deleteNote,
      setNoteStart,
      setNoteLength,
      setNoteFrequency,
      setBpm,
      setBar,
      setBeat,
      setVisible,
      setLength,
      setDrawLength,
      getWorkspaceRef,
      isOver,
      isPlaying,
      isLoaded,
      bpm,
      bar,
      beat,
      visible,
      length,
      frequencyHeight,
      drawLength,
      scrollTop,
      scrollLeft,
      elapsed,
      notes,
    } = this.props
    
    return connectDropTarget(
      <div className="ad-Composer">
        <Head
          play={ play }
          pause={ pause }
          stop={ stop }
          setBpm={ setBpm }
          setBar={ setBar }
          setBeat={ setBeat }
          setVisible={ setVisible }
          setLength={ setLength }
          isPlaying={ isPlaying }
          isLoaded={ isLoaded } 
          bpm={ bpm }
          bar={ bar }
          beat={ beat }
          visible={ visible }
          length={ length } />
        
        <Workspace
          play={ play }
          pause={ pause }
          setStart={ setStart }
          createNote={ createNote }
          deleteNote={ deleteNote }
          setNoteStart={ setNoteStart }
          setNoteLength={ setNoteLength }
          setNoteFrequency={ setNoteFrequency }
          setDrawLength={ setDrawLength }
          getWorkspaceRef={ getWorkspaceRef }
          isPlaying={ isPlaying }
          bar={ bar }
          beat={ beat }
          length={ length }
          frequencyHeight={ frequencyHeight }
          drawLength={ drawLength }
          scrollTop={ scrollTop }
          scrollLeft={ scrollLeft }
          visible={ visible }
          elapsed={ elapsed }
          notes={ notes } />
        
        <Layer
          length={ length }
          frequencyHeight={ frequencyHeight } />
        
        <Overlay isOver={ isOver } />
      </div>
    )
  }
}

class Head extends PureComponent {
  static propTypes = {
    play: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    stop: PropTypes.func.isRequired,
    setBpm: PropTypes.func.isRequired,
    setBar: PropTypes.func.isRequired,
    setBeat: PropTypes.func.isRequired,
    setVisible: PropTypes.func.isRequired,
    setLength: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    isLoaded: PropTypes.bool.isRequired,
    bpm: PropTypes.number.isRequired,
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
  };
  
  handlePlayClick = () => this.props.play();

  handlePauseClick = () => this.props.pause();

  handleStopClick = () => this.props.stop();
  
  render() {
    const {
      setBpm,
      setBar,
      setBeat,
      setVisible,
      setLength,
      isPlaying,
      isLoaded,
      bpm,
      bar,
      beat,
      visible,
      length,
    } = this.props
    
    return (
      <div className="ad-Head">
        <div className="ad-Head-actions">
          { isPlaying ? (
            <Button
              onClick={ this.handlePauseClick }
              className="ad-Button--pause">
              <MdPause />
            </Button>
          ) : (
            <Button
              onClick={ isLoaded && this.handlePlayClick }
              className={ cx([
                "ad-Button--play",
                !isLoaded && "ad-Button--disabled",
              ]) }>
              <MdPlayArrow />
            </Button>
          ) }
          <Button
            onClick={ isLoaded && this.handleStopClick }
            className={ cx([
              "ad-Button--stop",
              !isLoaded && "ad-Button--disabled",
            ]) }>
            <MdStop />
          </Button>
        </div>
        
        <div className="ad-Head-settings">
          <Setting
            onChange={ setBpm }
            title="BPM"
            value={ bpm }
            min={ 1 }
            max={ 300 } />
          <Setting
            onChange={ setBar }
            title="Bar"
            value={ bar }
            min={ 1 }
            max={ 8 } />
          <Setting
            onChange={ setBeat }
            title="Beat"
            value={ beat }
            min={ 1 }
            max={ 8 } />
          <Setting
            onChange={ setLength }
            title="Length"
            value={ length }
            min={ bar * beat } />
          <Setting
            onChange={ setVisible }
            title="Visible"
            value={ visible }
            min={ bar * beat }
            max={ length } />
        </div>
      </div>
    )
  }
}

class Setting extends PureComponent {
  constructor(props) {
    super(props)
    this.isFocused = false
  }

  static propTypes = {
    onChange: PropTypes.func.isRequired,
    title: PropTypes.string.isRequired,
    value: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.number,
    ]).isRequired,
    min: PropTypes.number,
    max: PropTypes.number,
  };

  state = { value: this.props.value };

  componentWillReceiveProps(nextProps) {
    if (!this.isFocused && this.state.value !== nextProps.value) {
      this.setState({ value: nextProps.value })
    }
  }

  handleFocus = () => this.isFocused = true;

  handleBlur = (e) => {
    e.persist()

    this.setState({ value: e.target.value }, () => {
      this.isFocused = false
      this.props.onChange(this.makeSureValueIsANumber(e.target.value))
    })
  };

  handleChange = (e) => this.setState({ value: e.target.value });

  makeSureValueIsANumber = (_value) => {
    const { min, max } = this.props
    let value = parseFloat(_value)

    if (isNaN(value)) {
      value = typeof min !== "undefined" ? min : 0
    }

    if (typeof min !== "undefined") {
      value = Math.max(value, min)
    }

    if (typeof max !== "undefined") {
      value = Math.min(value, max)
    }
    
    return value
  };

  render() {
    const {
      onChange,
      title,
      value,
      min,
      max,
      ...props,
    } = this.props

    return (
      <label className="ad-Setting">
        <div className="ad-Setting-title">
          { title }
        </div>
        <div className="ad-Setting-value">
          <input
            type="text"
            className="ad-Setting-input"
            onChange={ this.handleChange }
            onFocus={ this.handleFocus }
            onBlur={ this.handleBlur }
            value={ this.state.value }
            { ...props } />
        </div>
      </label>
    )
  }
}

class Workspace extends PureComponent {
  static propTypes = {
    play: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    setStart: PropTypes.func.isRequired,
    createNote: PropTypes.func.isRequired,
    deleteNote: PropTypes.func.isRequired,
    setNoteStart: PropTypes.func.isRequired,
    setNoteLength: PropTypes.func.isRequired,
    setNoteFrequency: PropTypes.func.isRequired,
    setDrawLength: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    frequencyHeight: PropTypes.number.isRequired,
    drawLength: PropTypes.number.isRequired,
    setScrollTop: PropTypes.number.isRequired,
    setScrollLeft: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    elapsed: PropTypes.number.isRequired,
    notes: PropTypes.arrayOf(types.note).isRequired,
  };

  render() {
    const {
      play,
      pause,
      setStart,
      createNote,
      deleteNote,
      setNoteStart,
      setNoteLength,
      setNoteFrequency,
      setDrawLength,
      getWorkspaceRef,
      isPlaying,
      bar,
      beat,
      length,
      frequencyHeight,
      drawLength,
      scrollTop,
      scrollLeft,
      visible,
      elapsed,
      notes,
    } = this.props
    
    return (
      <div
        ref={ getWorkspaceRef }
        className="ad-Workspace">
        <div
          className="ad-Workspace-frame"
          style={{ width: `${ (length / visible) * 100 }%` }}>
          <Frequencies
            frequencyHeight={ frequencyHeight }
            scrollTop={ scrollTop }
            scrollLeft={ scrollLeft } />

          <div className="ad-Workspace-content">
            <Marker
              elapsed={ elapsed }
              length={ length }
              scrollTop={ scrollTop } />
            
            <Scale
              play={ play }
              pause={ pause }
              setStart={ setStart }
              isPlaying={ isPlaying }
              bar={ bar }
              beat={ beat }
              visible={ visible }
              length={ length }
              scrollTop={ scrollTop } />
            
            <Notes
              createNote={ createNote }
              deleteNote={ deleteNote }
              setNoteStart={ setNoteStart }
              setNoteLength={ setNoteLength }
              setNoteFrequency={ setNoteFrequency }
              setDrawLength={ setDrawLength }
              length={ length }
              bar={ bar }
              beat={ beat }
              visible={ visible }
              length={ length }
              frequencyHeight={ frequencyHeight }
              drawLength={ drawLength }
              notes={ notes } />
          </div>
        </div>
      </div>
    )
  }
}

@DropTarget(
  [
    ItemTypes.NOTE,
    ItemTypes.NOTE_GRIP_LEFT,
    ItemTypes.NOTE_GRIP_RIGHT,
  ],
  {
    drop(props, monitor, component) {
      switch (monitor.getItemType()) {
      case ItemTypes.NOTE:
        return component.dropNote(props, monitor, component)

      case ItemTypes.NOTE_GRIP_LEFT:
        return component.dropNoteGripLeft(props, monitor, component)

      case ItemTypes.NOTE_GRIP_RIGHT:
        return component.dropNoteGripRight(props, monitor, component)
      }
    },
  },
  (connect, monitor) => ({
    connectDropTarget: connect.dropTarget(),
  }),
)
class Notes extends PureComponent {
  static propTypes = {
    connectDropTarget: PropTypes.func.isRequired,
    createNote: PropTypes.func.isRequired,
    deleteNote: PropTypes.func.isRequired,
    setNoteStart: PropTypes.func.isRequired,
    setNoteLength: PropTypes.func.isRequired,
    setNoteFrequency: PropTypes.func.isRequired,
    setDrawLength: PropTypes.func.isRequired,
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    frequencyHeight: PropTypes.number.isRequired,
    drawLength: PropTypes.number.isRequired,
    notes: PropTypes.arrayOf(types.note).isRequired,
  };

  state = {
    isPinching: false,
    lastStart: 0,
    lastEnd: 0,
    lastFrequency: sortedFrequencyTypes[0],
  };

  componentDidMount() {
    document.addEventListener("mousemove", this.handleMouseMove)
    document.addEventListener("mouseup", this.handleMouseUp)
  }

  componentWillUnmount() {
    document.removeEventListener("mousemove", this.handleMouseMove)
    document.removeEventListener("mouseup", this.handleMouseUp)
  }

  dropNoteGripLeft = (props, monitor, component) => {
    const { x } = monitor.getSourceClientOffset()
    const item = monitor.getItem()
    const { left, width } = findDOMNode(component).getBoundingClientRect()
    
    const {
      setNoteStart,
      setNoteLength,
      setDrawLength,
      length,
    } = props
    
    const end = item.start + item.length
    const start = utils.clamp(
      Math.round(((x - left) / width) * length),
      0,
      end - 1,
    )

    setNoteStart(item.id, start)
    setNoteLength(item.id, end - start)
    setDrawLength(end - start)
  };

  dropNoteGripRight = (props, monitor, component) => {
    const { x } = monitor.getSourceClientOffset()
    const item = monitor.getItem()
    const { left, width } = findDOMNode(component).getBoundingClientRect()

    const {
      setNoteLength,
      setDrawLength,
      length,
    } = props

    const end = utils.clamp(
      Math.ceil(((x - left) / width) * length),
      item.start + 1,
      length,
    )

    setNoteLength(item.id, end - item.start)
    setDrawLength(end - item.start)
  };

  dropNote = (props, monitor, component) => {
    const { x, y } = monitor.getSourceClientOffset()
    const item = monitor.getItem()

    const {
      setNoteStart,
      setNoteFrequency,
      length,
      frequencyHeight,
    } = props

    const {
      top,
      left,
      width,
    } = findDOMNode(component).getBoundingClientRect()

    const start = utils.clamp(
      Math.round(((x - left) / width) * length),
      0,
      length - item.length,
    )

    const index = utils.clamp(
      Math.round((y - top) / frequencyHeight),
      0,
      sortedFrequencyTypes.length - 1,
    )

    setNoteStart(item.id, start)
    setNoteFrequency(item.id, sortedFrequencyTypes[index])
  };

  getNoteFromMousePosition = (e) => {
    const {
      top,
      left,
      width,
    } = this.notes.getBoundingClientRect()

    const {
      length,
      frequencyHeight,
      drawLength,
    } = this.props
    
    const start = utils.clamp(
      Math.round(((e.clientX - left) / width) * length),
      0,
      length - drawLength,
    )
    
    const index = utils.clamp(
      Math.floor((e.clientY - top) / frequencyHeight),
      0,
      sortedFrequencyTypes.length - 1,
    )

    return {
      start,
      length: drawLength,
      frequency: sortedFrequencyTypes[index],
    }
  };

  handleMouseDown = (e) => {
    const { start, length, frequency } = this.getNoteFromMousePosition(e)
    
    this.setState({
      isPinching: true,
      lastStart: start,
      lastEnd: start + length,
      lastFrequency: frequency,
    })

    if (e.button === 0) {
      this.props.createNote(start, length, frequency)
    }
    
    e.preventDefault()
  };

  handleMouseMove = (e) => {
    if (this.state.isPinching && e.button === 0) {
      const { createNote } = this.props
      const { lastStart, lastEnd, lastFrequency } = this.state
      const { start, length } = this.getNoteFromMousePosition(e)
      
      // paint after
      if (start >= lastEnd) {
        const _end = lastEnd + length
        
        if (_end <= this.props.length) {
          this.setState(
            { lastEnd: _end },
            () => createNote(lastEnd, length, lastFrequency),
          )
        }
      }

      // paint before
      if (start <= lastStart - (length / 2)) {
        const _start = lastStart - length

        if (_start >= 0) {
          this.setState(
            { lastStart: _start },
            () => createNote(_start, length, lastFrequency),
          )
        }
      }
    }
  };

  handleMouseUp = () => this.setState({ isPinching: false });

  handleContextMenu = (e) => {
    e.preventDefault()
  };

  handleNoteMouseDown = (e, note) => {
    if (e.button === 0) {
      this.props.setDrawLength(note.length)
      e.stopPropagation()
    }
    
    if (e.button === 2) {
      this.props.deleteNote(note.id)
    }
  };

  handleNoteMouseOver = (e, note) => {
    if (this.state.isPinching && e.button === 2) {
      this.props.deleteNote(note.id)
    }
  };

  renderDraggableNote = (note) => {
    const {
      length,
      frequencyHeight,
    } = this.props
    
    const top = sortedFrequencyTypes.indexOf(note.frequency) * frequencyHeight
    const left = `${ (note.start / length) * 100 }%`
    const width = `${ (note.length / length) * 100 }%`
    
    return (
      <DraggableNote
        key={ note.id }
        onMouseDown={ this.handleNoteMouseDown }
        onMouseOver={ this.handleNoteMouseOver }
        top={ top }
        left={ left }
        width={ width }
        height={ frequencyHeight }
        id={ note.id }
        start={ note.start }
        length={ note.length }
        frequency={ note.frequency } />
    )
  };

  render() {
    const {
      connectDropTarget,
      bar,
      beat,
      visible,
      length,
      frequencyHeight,
      notes,
    } = this.props

    return connectDropTarget(
      <div
        ref={ (node) => this.notes = node }
        onContextMenu={ this.handleContextMenu }
        onMouseDown={ this.handleMouseDown }
        style={{ height: sortedFrequencyTypes.length * frequencyHeight }}
        className="ad-Notes">
        <Grid
          bar={ bar }
          beat={ beat }
          visible={ visible }
          length={ length }
          frequencyHeight={ frequencyHeight } />
        
        { notes.map(this.renderDraggableNote) }
      </div>
    )
  }
}

@DragSource(
  ItemTypes.NOTE,
  {
    beginDrag(props, monitor, component) {
      const node = findDOMNode(component)
      const parentNode = node.parentNode

      return {
        noteRect: node.getBoundingClientRect(),
        parentRect: parentNode.getBoundingClientRect(),
        id: props.id,
        length: props.length,
        frequency: props.frequency,
      }
    },
  },
  (connect, monitor) => ({
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging(),
  }),
)
class DraggableNote extends PureComponent {
  static propTypes = {
    connectDragSource: PropTypes.func.isRequired,
    connectDragPreview: PropTypes.func.isRequired,
    isDragging: PropTypes.bool.isRequired,
    onMouseDown: PropTypes.func.isRequired,
    onMouseOver: PropTypes.func.isRequired,
    top: PropTypes.number.isRequired,
    left: PropTypes.number.isRequired,
    width: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    id: PropTypes.number.isRequired,
    start: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    frequency: PropTypes.string.isRequired,
  };

  state = { isDragging: false };

  componentDidMount() {
    this.props.connectDragPreview(HTML5Backend.getEmptyImage())
  }

  handleMouseOver = (e) => {
    const {
      onMouseOver,
      id,
    } = this.props
    
    onMouseOver(e, { id })
  };

  handleMouseDown = (e) => {
    const {
      onMouseDown,
      id,
      length,
    } = this.props
    
    onMouseDown(e, { id, length })
  };

  beginDragGrip = () => {
    const {
      id,
      start,
      length,
      frequency,
    } = this.props

    const node = findDOMNode(this)
    const parentNode = node.parentNode
    
    this.setState({ isDragging: true })

    return {
      noteRect: node.getBoundingClientRect(),
      parentRect: parentNode.getBoundingClientRect(),
      id,
      start,
      length,
      frequency,
    }
  };

  endDragGrip = () => this.setState({ isDragging: false });

  render() {
    const {
      connectDragSource,
      onMouseOver,
      top,
      left,
      width,
      height,
      frequency,
    } = this.props
    
    const shouldHide = this.props.isDragging || this.state.isDragging
    
    return (
      <div
        className="ad-DraggableNote"
        onMouseDown={ this.handleMouseDown }
        onMouseOver={ this.handleMouseOver }
        style={{
          opacity: shouldHide ? 0 : 1,
          top,
          left,
          width,
          height,
        }}>
        <Grip
          className={ cx("ad-DraggableNote-grip", "ad-DraggableNote-grip--left") }
          type={ ItemTypes.NOTE_GRIP_LEFT }
          beginDrag={ this.beginDragGrip }
          endDrag={ this.endDragGrip } />

        { connectDragSource(
          <div className="ad-DraggableNote-note">
            <Note frequency={ frequency } />
          </div>
        ) }

        <Grip
          className={ cx("ad-DraggableNote-grip", "ad-DraggableNote-grip--right") }
          type={ ItemTypes.NOTE_GRIP_RIGHT }
          beginDrag={ this.beginDragGrip }
          endDrag={ this.endDragGrip } />
      </div>
    )
  }
}

@DragSource(
  (props) => props.type,
  {
    beginDrag(props, monitor, component) {
      return props.beginDrag(props, monitor, component)
    },
    endDrag(props, monitor, component) {
      return props.endDrag(props, monitor, component)
    },
  },
  (connect, monitor) => ({
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
  }),
)
class Grip extends PureComponent {
  static propTypes = {
    connectDragSource: PropTypes.func.isRequired,
    connectDragPreview: PropTypes.func.isRequired,
    beginDrag: PropTypes.func.isRequired,
    endDrag: PropTypes.func.isRequired,
    type: PropTypes.string.isRequired,
  };

  componentDidMount() {
    this.props.connectDragPreview(HTML5Backend.getEmptyImage())
  }
 
  render() {
    const {
      connectDragSource,
      className,
    } = this.props

    return connectDragSource(
      <div className={ className } />
    )
  }
}

@DragLayer((monitor) => ({
  item: monitor.getItem(),
  itemType: monitor.getItemType(),
  offset: monitor.getSourceClientOffset(),
  isDragging: monitor.isDragging(),
}))
class Layer extends PureComponent {
  static propTypes = {
    item: PropTypes.object,
    itemType: PropTypes.string,
    offset: PropTypes.shape({
      x: PropTypes.number.isRequired,
      y: PropTypes.number.isRequired,
    }),
    isDragging: PropTypes.bool.isRequired,
    length: PropTypes.number.isRequired,
    frequencyHeight: PropTypes.number.isRequired,
  };

  getNoteGripLeftStyles = () => {
    const {
      item,
      offset,
      length,
    } = this.props
    
    if (!offset) {
      return { display: "none" }
    }
    
    const noteWidth = (1 / length) * item.parentRect.width
    const right = item.noteRect.left + item.noteRect.width
    const width = utils.clamp(
      Math.round((right - offset.x) / noteWidth) * noteWidth,
      noteWidth,
      right - item.parentRect.left,
    )
    
    return {
      width,
      height: item.noteRect.height,
      transform: `translate(${ right - width }px, ${ item.noteRect.top }px)`,
      position: "relative",
    }
  };

  getNoteGripRightStyles = () => {
    const {
      item,
      offset,
      length,
    } = this.props
    
    if (!offset) {
      return { display: "none" }
    }
    
    const noteWidth = (1 / length) * item.parentRect.width
    const width = utils.clamp(
      Math.ceil((offset.x - item.noteRect.left) / noteWidth) * noteWidth,
      noteWidth,
      (item.parentRect.left + item.parentRect.width) - item.noteRect.left,
    )
    
    return {
      width,
      height: item.noteRect.height,
      transform: `translate(${ item.noteRect.left }px, ${ item.noteRect.top }px)`,
      position: "relative",
    }
  };
 
  getNoteStyles = () => {
    const {
      item,
      offset,
      length,
      frequencyHeight,
    } = this.props
    
    if (!offset) {
      return { display: "none" }
    }
    
    const noteWidth = (1 / length) * item.parentRect.width
    
    const dx = utils.clamp(
      Math.round((offset.x - item.parentRect.left) / noteWidth) * noteWidth,
      0,
      item.parentRect.width - item.noteRect.width,
    )
    
    const dy = utils.clamp(
      Math.round((offset.y - item.parentRect.top) / frequencyHeight) * frequencyHeight,
      0,
      item.parentRect.height - item.noteRect.height,
    )
    
    return {
      width: item.noteRect.width,
      height: item.noteRect.height,
      transform: `translate(${ item.parentRect.left + dx }px, ${ item.parentRect.top + dy }px)`,
      position: "relative",
    }
  };

  getFrequency = () => {
    const {
      item,
      offset,
      frequencyHeight,
    } = this.props
    
    if (!offset) {
      return null
    }
    
    const index = utils.clamp(
      Math.round((offset.y - item.parentRect.top) / frequencyHeight),
      0,
      sortedFrequencyTypes.length - 1,
    )
    
    return sortedFrequencyTypes[index]
  };
  
  renderItem = () => {
    const {
      item,
      itemType,
    } = this.props

    switch (itemType) {
    case ItemTypes.NOTE:
      return (
        <div style={ this.getNoteStyles() }>
          <Note frequency={ this.getFrequency() } />
        </div>
      )

    case ItemTypes.NOTE_GRIP_LEFT:
      return (
        <div style={ this.getNoteGripLeftStyles() }>
          <div className={ cx(
            "ad-DraggableNote-grip",
            "ad-DraggableNote-grip--left",
            "ad-DraggableNote-grip--show",
          ) } />
          <Note frequency={ item.frequency } />
        </div>
      )

    case ItemTypes.NOTE_GRIP_RIGHT:
      return (
        <div style={ this.getNoteGripRightStyles() }>
          <Note frequency={ item.frequency } />
          <div className={ cx(
            "ad-DraggableNote-grip",
            "ad-DraggableNote-grip--right",
            "ad-DraggableNote-grip--show",
          ) } />
        </div>
      )

    default:
      return null
    }
  };
  
  render() {
    const {
      isDragging,
    } = this.props
    
    if (!isDragging) {
      return null
    }
    
    return (
      <div className="ad-Layer">
        { this.renderItem() }
      </div>
    )
  }
}

class Note extends PureComponent {
  static propTypes = {
    frequency: PropTypes.string.isRequired,
  };

  render() {
    const { frequency } = this.props

    return (
      <div className="ad-Note">
        { frequency }
      </div>
    )
  }
}

class Grid extends PureComponent {
  static propTypes = {
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    frequencyHeight: PropTypes.number.isRequired,
  };

  renderVerticalLines = () => {
    const {
      bar,
      beat,
      length,
      visible,
    } = this.props
    
    const notesPerBar = bar * beat
    const bars = length / notesPerBar
    const units = bars
    
    return (
      <g>
        { Array.from({ length: units }).map((_, index) => (
          <line
            key={ index }
            className="ad-Grid-line"
            x1={ `${ index * (100 / units) }%` }
            y1={ 0 }
            x2={ `${ index * (100 / units) }%` }
            y2={ `100%` } />
        )) }
      </g>
    )
  };

  renderHorizontalLines = () => {
    const { frequencyHeight } = this.props
    
    return (
      <g>
        { sortedFrequencyTypes.map((type, index) => (
          <line
            key={ index }
            className={ cx([
              "ad-Grid-line",
              index % 12 === 0 && "ad-Grid-line--octave",
            ]) }
            x1={ 0 }
            y1={ index * frequencyHeight }
            x2={ `100%` }
            y2={ index * frequencyHeight } />
        )) }
      </g>
    )
  };

  render() {
    const { frequencyHeight } = this.props

    return (
      <svg
        width={ `100%` }
        height={ frequencyHeight * sortedFrequencyTypes.length }
        className="ad-Grid">
        { this.renderHorizontalLines() }
        { this.renderVerticalLines() }
      </svg>
    )
  }
}

class Scale extends PureComponent {
  constructor(props) {
    super(props)
    this.wasPlaying = false
  }
  
  static propTypes = {
    play: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    setStart: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    bar: PropTypes.number.isRequired,
    beat: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    visible: PropTypes.number.isRequired,
    scrollTop: PropTypes.number.isRequired,
  };

  state = { isDragging: false };

  componentDidMount() {
    document.addEventListener("mousemove", this.handleMouseMove)
    document.addEventListener("mouseup", this.handleMouseUp)
  }
  
  componentWillUnmount() {
    document.removeEventListener("mousemove", this.handleMouseMove)
    document.removeEventListener("mouseup", this.handleMouseUp)
  }

  setStartFromEvent = (e) => {
    const { setStart, length } = this.props
    const { left, width } = this.scale.getBoundingClientRect()
    const start = utils.clamp(
      Math.round(((e.clientX - left) / width) * length),
      0,
      length,
    )

    setStart(start)
  };

  handleMouseDown = (e) => {    
    const {
      pause,
      isPlaying,
    } = this.props
    
    if (isPlaying) {
      this.wasPlaying = true
      pause()
    }
    
    this.setStartFromEvent(e)
    this.setState({ isDragging: true })
    e.preventDefault()
  };

  handleMouseMove = (e) => {
    const { isDragging } = this.state
    
    if (isDragging) {
      this.setStartFromEvent(e)
      e.preventDefault()
    }
  };

  handleMouseUp = () => {
    const { play } = this.props
    
    if (this.wasPlaying) {
      this.wasPlaying = false
      play()
    }
    
    this.setState({ isDragging: false })
  };

  renderUnit = (_, index) => {
    const unit = index + 1
    
    return (
      <div
        key={ index }
        className="ad-Scale-unit">
        { unit }
      </div>
    )
  };

  render() {
    const {
      bar,
      beat,
      length,
      visible,
      scrollTop,
    } = this.props
    
    const notesPerBar = bar * beat
    const bars = length / notesPerBar
    const units = bars
    
    return (
      <div
        ref={ (node) => this.scale = node }
        className="ad-Scale"
        onMouseDown={ this.handleMouseDown }
        style={{ transform: `translateY(${ scrollTop }px)` }}>
        { Array.from({ length: units }).map(this.renderUnit) }
      </div>
    )
  }
}

class Marker extends PureComponent {
  static propTypes = {
    elapsed: PropTypes.number.isRequired,
    length: PropTypes.number.isRequired,
    scrollTop: PropTypes.number.isRequired,
  };

  render() {
    const {
      elapsed,
      length,
      scrollTop,
    } = this.props
    
    return (
      <div
        className="ad-Marker"
        style={{
          left: `${ (elapsed / length) * 100 }%`,
          transform: `translateY(${ scrollTop }px)`,
        }} />
    )
  }
}

class Frequencies extends PureComponent {
  static propTypes = {
    frequencyHeight: PropTypes.number.isRequired,
    scrollTop: PropTypes.number.isRequired,
    scrollLeft: PropTypes.number.isRequired,
  };
  
  renderFrequency = (frequency, index) => {
    const { frequencyHeight } = this.props
    
    return (
      <div
        key={ index }
        style={{ height: frequencyHeight }}
        className={ cx([
          "ad-Frequency",
          utils.isSharp(frequency) && "ad-Frequency--sharp",
          index % 12 === 0 && "ad-Frequency--octave",
        ]) }>
        { frequency }
      </div>
    )
  };
  
  render() {
    const {
      scrollTop,
      scrollLeft,
    } = this.props

    return (
      <div
        className="ad-Frequencies"
        style={{ transform: `translateX(${ scrollLeft }px)` }}>
        <div
          className="ad-Frequencies-patch"
          style={{ transform: `translateY(${ scrollTop }px)` }} />
        
        { sortedFrequencyTypes.map(this.renderFrequency) }
      </div>
    )
  }
}

class Modal extends PureComponent {
  static propTypes = {
    loadState: PropTypes.func.isRequired,
    closeModal: PropTypes.func.isRequired,
    isOpened: PropTypes.bool.isRequired,
  };

  render() {
    const {
      loadState,
      closeModal,
      isOpened,
    } = this.props
    
    if (!isOpened) {
      return null
    }

    return (
      <div className="ad-Modal">
        <div className="ad-Modal-content">
          <div className="ad-Modal-head">
            Welcome on the React Melody Composer!
          </div>
          
          <div className="ad-Modal-body">
            <div className="ad-Modal-intro">
              This is an experiment made with React and the Web Audio API.<br />
              Unleash your creativity and compose your own melody!
            </div>

            <ul className="ad-Modal-features">
              <li className="ad-Modal-feature">
                <span className="ad-Modal-action">Left click + hold</span>
                <span className="ad-Modal-hint">Paint notes</span>
              </li>
              <li className="ad-Modal-feature">
                <span className="ad-Modal-action">Right click + hold</span>
                <span className="ad-Modal-hint">Remove notes</span>
              </li>
              <li className="ad-Modal-feature">
                <span className="ad-Modal-action">Drag a note</span>
                <span className="ad-Modal-hint">Move/Resize the note</span>
              </li>
              <li className="ad-Modal-feature">
                <span className="ad-Modal-action">Drop an audio file</span>
                <span className="ad-Modal-hint">Load a custom sample</span>
              </li>
            </ul>
          </div>

          <div className="ad-Modal-foot">
            <div className="ad-Modal-title">
              Let's make a cool song!
            </div>

            <div className="ad-Modal-examples">
              <ButtonNew
                onClick={ () => loadState("https://s3-us-west-2.amazonaws.com/s.cdpn.io/80862/fur-elise-beethoven.json") }
                icon={ <MdQueueMusic /> }
                title="Für Elise"
                subtitle="Beethoven" />
              <ButtonNew
                onClick={ () => loadState("https://s3-us-west-2.amazonaws.com/s.cdpn.io/80862/orchestral-suite-no3-bach.json") }
                icon={ <MdQueueMusic /> }
                title="Orchestral Suite No. 3"
                subtitle="Bach" />
              <ButtonNew
                onClick={ closeModal }
                icon={ <MdAdd /> }
                title="New"
                subtitle="Empty" />
            </div>

            <div className="ad-Modal-more">
              Like this experiment? <a href="https://twitter.com/a_dugois" target="_blank">Follow me on Twitter</a> for more!
            </div>
          </div>
        </div>
      </div>
    )
  }
}

class Overlay extends PureComponent {
  static propTypes = {
    isOver: PropTypes.bool.isRequired,
  };

  render() {
    const { isOver } = this.props

    return (
      <div className={ cx([
        "ad-Overlay",
        isOver && "is-over",
      ]) }>
        Drop a new sample
      </div>
    )
  }
}

class ButtonNew extends PureComponent {
  static propTypes = {
    icon: PropTypes.element.isRequired,
    title: PropTypes.string.isRequired,
    subtitle: PropTypes.string.isRequired,
  };
  
  render() {
    const {
      icon,
      title,
      subtitle,
      ...props,
    } = this.props
    
    return (
      <Button
        className="ad-ButtonNew"
        { ...props }>
        <div className="ad-ButtonNew-content">
          <div className="ad-ButtonNew-icon">
            { icon }
          </div>
          <div className="ad-ButtonNew-title">
            { title }
          </div>
          { subtitle && (
            <div className="ad-ButtonNew-subtitle">
              { subtitle }
            </div>
          ) }
        </div>
      </Button>
    )
  }
}

class Button extends PureComponent {
  render() {
    const {
      children,
      className,
      ...props,
    } = this.props
    
    return (
      <button
        type="button"
        className={ cx(["ad-Button", className]) }
        { ...props }>
        <div className="ad-Button-content">
          { children }
        </div>
      </button>
    )
  }
}

class IconBase extends PureComponent {
  render() {
    const {
      children,
      ...props,
    } = this.props
    
    return (
      <svg
        fill="currentColor"
        preserveAspectRatio="xMidYMid meet"
        width="1em"
        height="1em"
        { ...props }>
        { children }
      </svg>
    )
  }
}

class MdPlayArrow extends PureComponent {
  render() {
    return (
      <IconBase
        viewBox="0 0 40 40"
        { ...this.props }>
        <path d="m13.4 8.4l18.2 11.6-18.2 11.6v-23.2z" />
      </IconBase>
    )
  }
}

class MdPause extends PureComponent {
  render() {
    return (
      <IconBase
        viewBox="0 0 40 40"
        { ...this.props }>
        <path d="m23.4 8.4h6.6v23.2h-6.6v-23.2z m-13.4 23.2v-23.2h6.6v23.2h-6.6z" />
      </IconBase>
    )
  }
}

class MdStop extends PureComponent {
  render() {
    return (
      <IconBase
        viewBox="0 0 40 40"
        { ...this.props }>
        <path d="m10 10h20v20h-20v-20z" />
      </IconBase>
    )
  }
}

class MdQueueMusic extends PureComponent {
  render() {
    return (
      <IconBase
        viewBox="0 0 40 40"
        { ...this.props }>
        <path d="m28.4 10h8.2v3.4h-5v15c0 2.7-2.2 5-5 5s-5-2.3-5-5 2.3-5 5-5c0.6 0 1.2 0.1 1.8 0.3v-13.7z m-23.4 16.6v-3.2h13.4v3.2h-13.4z m20-10v3.4h-20v-3.4h20z m0-6.6v3.4h-20v-3.4h20z" />
      </IconBase>
    )
  }
}

class MdAdd extends PureComponent {
  render() {
    return (
      <IconBase
        viewBox="0 0 40 40"
        { ...this.props }>
        <path d="m31.6 21.6h-10v10h-3.2v-10h-10v-3.2h10v-10h3.2v10h10v3.2z" />
      </IconBase>
    )
  }
}

render(
  <Root />,
  document.querySelector("#root"),
)
            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console