⌈⌋ ⎇ branch:  Bitrhythm


Artifact Content

Artifact e8ca832a264a38c8a2ae147711d700ae9e475ad1957c12ece21023c52b415031:


'use strict';

let svg = require('../util/svg');
let Interface = require('../core/interface');
let ButtonTemplate = require('../components/buttontemplate');
let touch = require('../util/touch');

class PianoKey extends ButtonTemplate {

  constructor() {

    let options = ['value','note','color'];

    let defaults = {
      'size': [80,80],
      'target': false,
      'mode': 'button',
      'value': 0
    };

    super(arguments,options,defaults);

    this.note = this.settings.note;
    this.color = this.settings.color;

    this.colors = {
      'w': '#fff',
      'b': '#666',
    };

    this.init();
    this.render();

  }

  buildFrame() {
    this.element = svg.create('svg');
    this.element.setAttribute('width',this.width);
    this.element.setAttribute('height',this.height);
    this.parent.appendChild(this.element);
  }

  buildInterface() {

    this.pad = svg.create('rect');

    this.element.appendChild(this.pad);

    this.interactionTarget = this.pad;

    /* events */

    if (!touch.exists) {

      this.click = () => {
      //  console.log('click');
        this.piano.interacting = true;
        this.piano.paintbrush = !this.state;
        this.down(this.piano.paintbrush);
      };

      this.pad.addEventListener('mouseover', () => {
        if (this.piano.interacting) {
      //    console.log('mouseover');
          this.down(this.piano.paintbrush);
        }
      });


      this.move = () => {
        if (this.piano.interacting) {
        //  console.log('move');
          this.bend();
        }
      };


      this.release = () => {
        this.piano.interacting = false;
      //  console.log('release');
      //  this.up();
      };
      this.pad.addEventListener('mouseup', () => {
        if (this.piano.interacting) {
        //  console.log('mouseup');
          this.up();
        }
      });
      this.pad.addEventListener('mouseout', () => {
        if (this.piano.interacting) {
        //  console.log('mouseout');
          this.up();
        }
      });

    }

  }

  sizeInterface() {

        //let radius = Math.min(this.width,this.height) / 5;
        let radius = 0;

        this.pad.setAttribute('x',0.5);
        this.pad.setAttribute('y',0.5);
        if (this.width > 2) {
          this.pad.setAttribute('width', this.width - 1);
        } else {
          this.pad.setAttribute('width', this.width);
        }
        if (this.height > 2) {
          this.pad.setAttribute('height', this.height);
        } else {
          this.pad.setAttribute('height', this.height);
        }
        this.pad.setAttribute('rx', radius);
        this.pad.setAttribute('ry', radius);

  }

  render() {
    if (!this.state) {
      this.pad.setAttribute('fill', this.colors[this.color]);
    } else {
      this.pad.setAttribute('fill', this.colors.accent);
    }
  }

}

/**
* Piano
*
* @description Piano keyboard interface
*
* @demo <div nexus-ui="piano"></div>
*
* @example
* var piano = new Nexus.Piano('#target')
*
* @example
* var piano = new Nexus.Piano('#target',{
*     'size': [500,125],
*     'mode': 'button',  // 'button', 'toggle', or 'impulse'
*     'lowNote': 24,
*     'highNote': 60
* })
*
* @output
* change
* Fires any time a new key is pressed or released <br>
* The event data is an object containing <i>note</i> and <i>state</i> properties.
*
* @outputexample
* piano.on('change',function(v) {
*   console.log(v);
* })
*
*/

export default class Piano extends Interface {

  constructor() {

    let options = ['value'];

    let defaults = {
      'size': [500,125],
      'lowNote': 24,
      'highNote': 60,
      'mode': 'button'
    };

    super(arguments,options,defaults);

    this.keyPattern = ['w','b','w','b','w','w','b','w','b','w','b','w'];

    this.paintbrush = false;

    this.mode = this.settings.mode;

    this.range = {
      low: this.settings.lowNote,
      high: this.settings.highNote
    };

    this.range.size = this.range.high - this.range.low + 1;

    this.keys = [];

    this.toggleTo = false;

    this.init();
    this.render();

  }

  buildFrame() {
    this.element = document.createElement('div');
    this.element.style.position = 'relative';
    this.element.style.borderRadius = '0px';
    this.element.style.display = 'block';
    this.element.style.width = '100%';
    this.element.style.height = '100%';
    this.parent.appendChild(this.element);
  }

  buildInterface() {

    this.keys = [];

    for (let i=0;i<this.range.size;i++) {

      let container = document.createElement('span');
      let scaleIndex = (i+this.range.low) % this.keyPattern.length;

      let key = new PianoKey(container, {
          component: true,
          note: i+this.range.low,
          color: this.keyPattern[scaleIndex],
          mode: this.mode
        }, this.keyChange.bind(this,i+this.range.low));

      key.piano = this;

      if (touch.exists) {
        key.pad.index = i;
        key.preClick = key.preMove = key.preRelease = () => {};
        key.click = key.move = key.release = () => {};
        key.preTouch = key.preTouchMove = key.preTouchRelease = () => {};
        key.touch = key.touchMove = key.touchRelease = () => {};
      }

      this.keys.push(key);
      this.element.appendChild(container);

    }
    if (touch.exists) {
      this.addTouchListeners();
    }

  }

  sizeInterface() {

    let keyX = 0;

    let keyPositions = [];

    for (let i=0;i<this.range.size;i++) {

      keyPositions.push(keyX);

      let scaleIndex = (i+this.range.low) % this.keyPattern.length;
      let nextScaleIndex = (i+1+this.range.low) % this.keyPattern.length;
      if (i+1+this.range.low >= this.range.high) {
        keyX += 1;
      } else if (this.keyPattern[scaleIndex] === 'w' && this.keyPattern[nextScaleIndex] === 'w') {
        keyX += 1;
      } else {
        keyX += 0.5;
      }
    }
    let keysWide = keyX;


  //  let padding = this.width / 120;
    let padding = 1;
    let buttonWidth = (this.width-padding*2) / keysWide;
    let buttonHeight = (this.height-padding*2) / 2;

    for (let i=0;i<this.keys.length;i++) {

      let container = this.keys[i].parent;
      container.style.position = 'absolute';
      container.style.left = (keyPositions[i]*buttonWidth+padding) + 'px';
      if (this.keys[i].color === 'w') {
        container.style.top = (padding) + 'px';
        this.keys[i].resize(buttonWidth, buttonHeight*2);
      } else {
        container.style.zIndex = 1;
        container.style.top = padding+'px';
        this.keys[i].resize(buttonWidth, buttonHeight*1.1);
      }

    }

  }

  colorInterface() {

    // Piano keys don't actually have a stroke border
    // They have space between them, which shows the Piano bg color
    this.element.style.backgroundColor = this.colors.mediumLight;

    for (let i=0;i<this.keys.length;i++) {
      this.keys[i].colors = {
        'w': this.colors.light,
        'b': this.colors.dark,
        'accent': this.colors.accent,
        'border': this.colors.mediumLight
      };
      this.keys[i].colorInterface();
      this.keys[i].render();
    }


  }

  keyChange(note,on) {
    // emit data for any key turning on/off
    // "note" is the note value
    // "on" is a boolean whether it is on or off
    // in aftertouch mode, "on: is an object with state/x/y properties
    var data = {
      note: note
    };
    if (typeof on === 'object') {
      data.state = on.state;
    //  data.x = on.x
    //  data.y = on.y
    } else {
      data.state = on;
    }
    this.emit('change',data);
  }

  /* drag(note,on) {
    this.emit('change',{
      note: note,
      state: on
    });
  } */

  render() {
    // loop through and render the keys?
  }

  addTouchListeners() {
    this.preClick = this.preMove = this.preRelease = () => {};
    this.click = this.move = this.release = () => {};
    this.preTouch = this.preTouchMove = this.preTouchRelease = () => {};
    this.touch = this.touchMove = this.touchRelease = () => {};

    const allActiveTouches = {};
    const keys = this.keys;

    function cloneTouch(touch) {
      return { identifier: touch.identifier, clientX: touch.clientX, clientY: touch.clientY };
    }

    function updateKeyState() {
      const allActiveKeys = {};

      // Check/set "key-down" status for all keys that are currently touched.
      Object.keys(allActiveTouches).forEach(id => {
        const touch = allActiveTouches[id];
        const el = document.elementFromPoint(touch.clientX, touch.clientY);
        let key = el ? keys[el.index] : null;
        if (key) {
          allActiveKeys[el.index] = id;
          if (!key.state) {
            key.down();
          }
        } else {
          delete allActiveTouches[id];
        }
      });

      // Set "key-up" status for all keys that are untouched.
      keys.forEach(key => {
        if (key.state && !allActiveKeys[key.pad.index]) {
          key.up();
        }
      });
    }

    function handleTouchStartAndMove(e) {
      e.preventDefault();
      e.stopPropagation();
      for (let i = 0; i < e.changedTouches.length; i++){
        const touch = e.changedTouches[i];
        allActiveTouches[touch.identifier] = cloneTouch(touch);
      }
      updateKeyState();
    }

    function handleTouchEnd(e) {
      e.preventDefault();
      e.stopPropagation();
      for (let i = 0; i < e.changedTouches.length; i++){
        const touch = e.changedTouches[i];
        delete allActiveTouches[touch.identifier];
      }
      updateKeyState();
    }

    this.element.addEventListener('touchstart', handleTouchStartAndMove);
    this.element.addEventListener('touchmove', handleTouchStartAndMove);
    this.element.addEventListener('touchend', handleTouchEnd);
  }

  /**
  Define the pitch range (lowest and highest note) of the piano keyboard.
  @param low {number} MIDI note value of the lowest note on the keyboard
  @param high {number} MIDI note value of the highest note on the keyboard
  */
  setRange(low,high) {
    this.range.low = low;
    this.range.high = high;
    this.empty();
    this.buildInterface();
  }

  /**
  Turn a key on or off using its MIDI note value;
  @param note {number} MIDI note value of the key to change
  @param on {boolean} Whether the note should turn on or off
  */
  toggleKey(note, on) {
    this.keys[note-this.range.low].flip(on);
  }

  /**
  Turn a key on or off using its key index on the piano interface.
  @param index {number} Index of the key to change
  @param on {boolean} Whether the note should turn on or off
  */
  toggleIndex(index, on) {
    this.keys[index].flip(on);
  }

}