'use strict';
let svg = require('../util/svg');
let dom = require('../util/dom');
let Interface = require('../core/interface');
let ButtonTemplate = require('../components/buttontemplate');
let MatrixModel = require('../models/matrix');
let CounterModel = require('../models/counter');
let Interval = require('../time/interval');
let touch = require('../util/touch');
class MatrixCell extends ButtonTemplate {
constructor() {
let options = ['value'];
let defaults = {
size: [80, 80],
target: false,
mode: 'toggle',
value: 0,
paddingRow: 2,
paddingColumn: 2
};
super(arguments, options, defaults);
this.index = this.settings.index;
this.row = this.settings.row;
this.column = this.settings.column;
this.matrix = this.settings.matrix;
/**
* Amount of row padding
* @type {number}
*/
this.paddingRow = this.settings.paddingRow || defaults.paddingRow;
/**
* Amount of column padding
* @type {number}
*/
this.paddingColumn = this.settings.paddingColumn || defaults.paddingColumn;
this.interacting = false;
this.paintbrush = false;
this.init();
this.render();
}
buildFrame() {
this.element = svg.create('svg');
this.element.setAttribute('width', this.width);
this.element.setAttribute('height', this.height);
this.element.style.top = '0px';
this.element.style.left = '0px';
this.element.style.position = 'absolute';
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 = () => {
this.matrix.interacting = true;
this.matrix.paintbrush = !this.state;
this.down(this.matrix.paintbrush);
};
this.pad.addEventListener('mouseover', () => {
if (this.matrix.interacting) {
this.down(this.matrix.paintbrush);
}
});
this.move = () => {};
this.pad.addEventListener('mousemove', e => {
if (this.matrix.interacting) {
if (!this.offset) {
this.offset = dom.findPosition(this.element);
}
this.mouse = dom.locateMouse(e, this.offset);
this.bend();
}
});
this.release = () => {
this.matrix.interacting = false;
};
this.pad.addEventListener('mouseup', () => {
if (this.matrix.interacting) {
this.up();
}
});
this.pad.addEventListener('mouseout', () => {
if (this.matrix.interacting) {
this.up();
}
});
}
}
sizeInterface() {
this.pad.setAttribute('x', this.paddingColumn / 2);
this.pad.setAttribute('y', this.paddingRow / 2);
if (this.width > 2) {
this.pad.setAttribute('width', this.width - this.paddingColumn);
} else {
this.pad.setAttribute('width', this.width);
}
if (this.height > 2) {
this.pad.setAttribute('height', this.height - this.paddingRow);
} else {
this.pad.setAttribute('height', this.height);
}
this.pad.setAttribute('fill', this.matrix.colors.fill);
}
render() {
if (!this.state) {
this.pad.setAttribute('fill', this.matrix.colors.fill);
} else {
this.pad.setAttribute('fill', this.matrix.colors.accent);
}
}
}
/**
* Sequencer
*
* @description Grid of buttons with built-in step sequencer.
*
* @demo <div nexus-ui="sequencer" style="width:400px;height:200px;"></div>
*
* @example
* var sequencer = new Nexus.Sequencer('#target')
*
* @example
* var sequencer = new Nexus.Sequencer('#target',{
* 'size': [400,200],
* 'mode': 'toggle',
* 'rows': 5,
* 'columns': 10,
* 'paddingRow': 10,
* 'paddingColumn': 20
*})
*
* @output
* change
* Fires whenever a value is received. For example, when clicking a cell from off to on. <br>
* The event data is an object containing <i>row</i> (number), <i>column</i> (number), and <i>state</i> (boolean) properties.
*
* @outputexample
* sequencer.on('change',function(v) {
* console.log(v);
* })
*
* @output
* step
* Fires any time the sequencer steps to the next column, in sequece mode. <br>
* The event data is an <i>array</i> containing all values in the column, <i>bottom row first</i>.
*
* @outputexample
* sequencer.on('step',function(v) {
* console.log(v);
* })
*/
export default class Sequencer extends Interface {
constructor() {
let options = ['value'];
let defaults = {
size: [400, 200],
mode: 'toggle',
rows: 5,
columns: 10
};
super(arguments, options, defaults);
this.active = -1;
/**
* Button interaction mode: see Button
* @type {string}
* @example button.mode = 'toggle';
*/
this.mode = this.settings.mode;
/**
* The interval object which controls timing and sequence scheduling.
* @type {interval}
*/
this.interval = new Interval(200, function() {}, false); // jshint ignore:line
/**
* A Matrix model containing methods for manipulating the sequencer's array of values. To learn how to manipulate the matrix, read about the matrix model.
* @type {matrix}
*/
this.matrix = new MatrixModel(this.settings.rows, this.settings.columns);
this.matrix.ui = this;
/**
* A Counter model which the sequencer steps through. For example, you could use this model to step through the sequencer in reverse, randomly, or in a drunk walk.
* @type {counter}
*/
this.stepper = new CounterModel(0, this.columns);
this.paddingRow = this.settings.paddingRow;
this.paddingColumn = this.settings.paddingColumn;
this.init();
}
buildFrame() {
this.element = document.createElement('div');
this.element.style.position = 'relative';
this.element.style.display = 'block';
this.element.style.width = '100%';
this.element.style.height = '100%';
this.parent.appendChild(this.element);
if (touch.exists) {
this.addTouchListeners();
}
}
buildInterface() {
this.cells = [];
for (let i = 0; i < this.matrix.length; i++) {
let location = this.matrix.locate(i);
// returns {row,col}
let container = document.createElement('span');
container.style.position = 'absolute';
let cell = new MatrixCell(
container,
{
component: true,
index: i,
row: location.row,
column: location.column,
mode: this.mode,
matrix: this,
paddingRow: this.paddingRow,
paddingColumn: this.paddingColumn
},
this.keyChange.bind(this, i)
);
// cell.matrix = this;
if (touch.exists) {
cell.pad.index = i;
cell.preClick = cell.preMove = cell.preRelease = () => {};
cell.click = cell.move = cell.release = () => {};
cell.preTouch = cell.preTouchMove = cell.preTouchRelease = () => {};
cell.touch = cell.touchMove = cell.touchRelease = () => {};
}
this.cells.push(cell);
this.element.appendChild(container);
}
this.sizeInterface();
}
sizeInterface() {
let cellWidth = this.width / this.columns;
let cellHeight = this.height / this.rows;
for (let i = 0; i < this.cells.length; i++) {
let container = this.cells[i].parent;
container.style.left = this.cells[i].column * cellWidth + 'px';
container.style.top = this.cells[i].row * cellHeight + 'px';
this.cells[i].resize(cellWidth, cellHeight);
}
}
colorInterface() {
for (var i = 0; i < this.cells.length; i++) {
this.cells[i].render();
}
}
update() {
// console.log("updating...")
//on = on || false;
this.matrix.iterate((r, c, i) => {
// console.log(this.matrix.pattern[r][c], this.cells[i].state);
if (this.matrix.pattern[r][c] !== this.cells[i].state) {
if (this.matrix.pattern[r][c] > 0) {
this.cells[i].turnOn();
} else {
this.cells[i].turnOff();
}
}
});
}
// update => cell.turnOn => cell.emit => keyChange (seq.emit) => matrix.set.cell => update
//
// interaction => keyChange => matrix.set.cell => update => cell.turnOn
// => emit
//
// set.cell => update => needs to emit.
keyChange(note, on) {
// emit data for any key turning on/off
// i is the note index
// v is whether it is on or off
let cell = this.matrix.locate(note);
// this.matrix.set.cell(cell.column,cell.row,on);
this.matrix.pattern[cell.row][cell.column] = on;
var data = {
row: cell.row,
column: cell.column,
state: on
};
this.emit('change', data);
}
render() {
if (this.stepper.value >= 0) {
this.matrix.iterate((r, c, i) => {
if (c === this.stepper.value) {
this.cells[i].pad.setAttribute('stroke', this.colors.mediumLight);
this.cells[i].pad.setAttribute('stroke-width', '1');
this.cells[i].pad.setAttribute('stroke-opacity', '1');
} else {
this.cells[i].pad.setAttribute('stroke', 'none');
}
});
}
}
/**
* Start sequencing
* @param {number} ms Beat tempo in milliseconds
*/
start(ms) {
this.interval.event = this.next.bind(this);
if (ms) {
this.interval.ms(ms);
}
this.interval.start();
}
/**
Stop sequencing
*/
stop() {
this.interval.stop();
}
/**
Manually jump to the next column and trigger the 'change' event. The "next" column is determined by your mode of sequencing.
*/
next() {
this.stepper.next();
this.emit('step', this.matrix.column(this.stepper.value).reverse());
this.render();
}
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 = () => {};
this.currentElement = false;
this.element.addEventListener('touchstart', e => {
let element = document.elementFromPoint(
e.targetTouches[0].clientX,
e.targetTouches[0].clientY
);
let cell = this.cells[element.index];
this.paintbrush = !cell.state;
cell.down(this.paintbrush);
this.currentElement = element.index;
e.preventDefault();
e.stopPropagation();
});
this.element.addEventListener('touchmove', e => {
let element = document.elementFromPoint(
e.targetTouches[0].clientX,
e.targetTouches[0].clientY
);
let cell = this.cells[element.index];
if (element.index !== this.currentElement) {
if (this.currentElement >= 0) {
let pastCell = this.cells[this.currentElement];
pastCell.up();
}
cell.down(this.paintbrush);
} else {
cell.bend();
}
this.currentElement = element.index;
e.preventDefault();
e.stopPropagation();
});
this.element.addEventListener('touchend', e => {
// no touches to calculate because none remaining
let cell = this.cells[this.currentElement];
cell.up();
this.interacting = false;
this.currentElement = false;
e.preventDefault();
e.stopPropagation();
});
}
/**
Number of rows in the sequencer
@type {number}
*/
get rows() {
return this.matrix.rows;
}
set rows(v) {
this.matrix.rows = v;
this.empty();
this.buildInterface();
this.update();
}
/**
Number of columns in the sequencer
@type {number}
*/
get columns() {
return this.matrix.columns;
}
set columns(v) {
this.matrix.columns = v;
this.stepper.max = v;
this.empty();
this.buildInterface();
this.update();
}
}