├── .nvmrc ├── screenshot.gif ├── .babelrc ├── .travis.yml ├── .editorconfig ├── src ├── index.js ├── LoopMarker.js ├── LineMarker.js ├── CanvasUI.js ├── Ruler.js ├── eventMixin.js ├── Props.js ├── Waveform.js └── SampleEditorView.js ├── test └── SampleEditorView.spec.js ├── .gitignore ├── karma.conf.js ├── LICENSE ├── webpack.config.js ├── package.json ├── index.html ├── README.md ├── .eslintrc └── dist ├── SampleEditorView.min.js ├── SampleEditorView.js.map └── SampleEditorView.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.10 2 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rikard-io/SampleEditorView/HEAD/screenshot.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","stage-2"], 3 | "plugins": ["babel-plugin-add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: required 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | addons: 9 | chrome: stable 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Rikard Lindstrom 3 | * @Filename: index.js 4 | */ 5 | 6 | 'use strict'; 7 | import SampleEditorView from './SampleEditorView'; 8 | 9 | // expose on window 10 | window.SampleEditorView = SampleEditorView; 11 | export default SampleEditorView; 12 | -------------------------------------------------------------------------------- /test/SampleEditorView.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, before */ 2 | 3 | import chai from 'chai'; 4 | import SampleEditorView from '../src/SampleEditorView.js'; 5 | 6 | chai.expect(); 7 | 8 | const expect = chai.expect; 9 | 10 | // ... to do :) 11 | 12 | let sampleEditorView; 13 | 14 | describe('SampleEditorView', () => { 15 | before(() => { 16 | sampleEditorView = new SampleEditorView(); 17 | }); 18 | describe('props', () => { 19 | it('should be an object', () => { 20 | expect(sampleEditorView.props).to.be.an('object'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/LoopMarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Rikard Lindstrom 3 | * @Filename: LoopMarker.js 4 | */ 5 | 6 | import CanvasUI from './CanvasUI'; 7 | 8 | const defaultProperties = { 9 | height: 10, 10 | color: '#222' 11 | }; 12 | 13 | class LoopMarker extends CanvasUI { 14 | 15 | constructor(props) { 16 | 17 | super(defaultProperties, props); 18 | 19 | } 20 | 21 | render() { 22 | let ctx = this.ctx; 23 | 24 | // full clear and width / height set 25 | let w = this.props.width; 26 | let h = this.props.height; 27 | 28 | ctx.fillStyle = this.props.color; 29 | ctx.fillRect(0, 0, w, h); 30 | 31 | return this; 32 | } 33 | 34 | } 35 | 36 | export default LoopMarker; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Remove some common IDE working directories 30 | .idea 31 | .vscode 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | let configuration = { 3 | 4 | customLaunchers: { 5 | Chrome_travis_ci: { 6 | base: 'ChromeHeadless', 7 | flags: ['--no-sandbox'] 8 | } 9 | }, 10 | 11 | frameworks: ['mocha'], 12 | 13 | files: [ 14 | {pattern: 'test/*.spec.js', watched: false}, 15 | {pattern: 'test/**/*.spec.js', watched: false} 16 | ], 17 | 18 | preprocessors: { 19 | // add webpack as preprocessor 20 | 'test/*.spec.js': ['webpack'], 21 | 'test/**/*.spec.js': ['webpack'] 22 | }, 23 | 24 | webpack: { 25 | }, 26 | 27 | webpackMiddleware: { 28 | // webpack-dev-middleware configuration 29 | // i. e. 30 | stats: 'errors-only' 31 | }, 32 | }; 33 | 34 | if (process.env.TRAVIS) { 35 | configuration.browsers = ['Chrome_travis_ci']; 36 | } else { 37 | configuration.browsers = ['ChromeHeadless']; 38 | } 39 | 40 | config.set(configuration); 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Rikard Lindström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/LineMarker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Rikard Lindstrom 3 | * @Filename: LineMarker.js 4 | */ 5 | 6 | import CanvasUI from './CanvasUI'; 7 | 8 | const defaultProperties = { 9 | background: '#999', 10 | color: '#222', 11 | width: 10, 12 | dir: 1 13 | }; 14 | 15 | class LineMarker extends CanvasUI { 16 | 17 | constructor(props) { 18 | 19 | super(defaultProperties, props); 20 | 21 | } 22 | 23 | render() { 24 | 25 | let ctx = this.ctx; 26 | 27 | // full clear and width / height set 28 | let w = this.props.width; 29 | let h = this.props.height; 30 | 31 | ctx.fillStyle = this.props.background; 32 | ctx.strokeStyle = this.props.color; 33 | 34 | ctx.beginPath(); 35 | 36 | let triH = h / 64; 37 | let triH2 = triH * 2; 38 | 39 | if (this.props.dir === 1) { 40 | ctx.lineTo(w, triH); 41 | ctx.lineTo(0, triH2); 42 | ctx.lineTo(0, 0); 43 | ctx.lineTo(w, triH); 44 | ctx.fill(); 45 | ctx.stroke(); 46 | ctx.strokeRect(0, 0, 1, h); 47 | } else { 48 | ctx.moveTo(w, 0); 49 | ctx.lineTo(0, triH); 50 | ctx.lineTo(w, triH2); 51 | ctx.lineTo(w, 0); 52 | ctx.lineTo(0, triH); 53 | ctx.fill(); 54 | ctx.stroke(); 55 | ctx.strokeRect(w, 0, 1, h); 56 | } 57 | 58 | return this; 59 | } 60 | 61 | } 62 | 63 | export default LineMarker; 64 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global __dirname, require, module*/ 2 | 3 | const webpack = require('webpack'); 4 | const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin; 5 | const path = require('path'); 6 | const env = require('yargs').argv.env; // use --env with webpack 2 7 | 8 | let projectName = 'SampleEditorView'; 9 | 10 | let plugins = [], outputFile; 11 | 12 | if (env === 'build') { 13 | plugins.push(new UglifyJsPlugin({ minimize: true })); 14 | outputFile = projectName + '.min.js'; 15 | } else { 16 | outputFile = projectName + '.js'; 17 | } 18 | 19 | const config = { 20 | entry: __dirname + '/src/index.js', 21 | devtool: 'source-map', 22 | output: { 23 | path: __dirname + '/dist', 24 | filename: outputFile, 25 | library: projectName, 26 | libraryTarget: 'umd', 27 | umdNamedDefine: true 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /(\.js)$/, 33 | loader: 'babel-loader', 34 | exclude: /(node_modules)/ 35 | }, 36 | { 37 | test: /(\.js)$/, 38 | loader: 'eslint-loader', 39 | exclude: /node_modules/, 40 | options: { 41 | fix:true 42 | } 43 | } 44 | ] 45 | }, 46 | resolve: { 47 | modules: [path.resolve('./node_modules'), path.resolve('./src')], 48 | extensions: ['.json', '.js'] 49 | }, 50 | plugins: plugins 51 | }; 52 | 53 | module.exports = config; 54 | -------------------------------------------------------------------------------- /src/CanvasUI.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base class for all ui 3 | * @Author: Rikard Lindstrom 4 | * @Filename: CanvasUI.js 5 | */ 6 | 7 | import Props from './Props'; 8 | 9 | const defaultProperties = { 10 | width: 640, 11 | height: 320, 12 | hZoom: 1, 13 | visible: true, 14 | duration: 'auto', 15 | color: '#fff', 16 | background: '#333', 17 | buffer: null 18 | }; 19 | 20 | class CanvasUI { 21 | 22 | constructor(defaults, props) { 23 | this.props = new Props(defaultProperties, defaults, props); 24 | this.props.$on('change', _ => { this.dirty = true; }); 25 | this.dirty = true; 26 | this.canvas = document.createElement('canvas'); 27 | this.ctx = this.canvas.getContext('2d'); 28 | this.ctx.imageSmoothingEnabled = false; 29 | } 30 | 31 | renderIfDirty() { 32 | if (!this.dirty) return this; 33 | this.clear(); 34 | if (!this.props.visible) return this; 35 | return this.render(); 36 | } 37 | 38 | clear() { 39 | // clear canvas and update width / height 40 | this.canvas.width = this.props.width; 41 | this.canvas.height = this.props.height; 42 | } 43 | 44 | get duration() { 45 | return this.props.duration === 'auto' ? (this.props.buffer ? this.props.buffer.duration : 0) : this.props.duration; 46 | } 47 | 48 | get displayDuration() { 49 | return this.duration / this.props.hZoom; 50 | } 51 | } 52 | 53 | export default CanvasUI; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-editor-view", 3 | "version": "1.0.0-b.1", 4 | "description": "A Canvas Renderer / Editor UI for AudioBuffers", 5 | "main": "dist/SampleEditorView.js", 6 | "scripts": { 7 | "build": "webpack --env dev && webpack --env build && npm run test", 8 | "dev": "webpack --progress --colors --watch --env dev", 9 | "test": "./node_modules/.bin/karma start --single-run", 10 | "test:mocha": "mocha-phantomjs", 11 | "test:watch": "mocha --require babel-core/register --colors -w ./test/*.spec.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/rikard-io/SampleEditorView.git" 16 | }, 17 | "keywords": [ 18 | "WebAudio", 19 | "Waveform", 20 | "Sampler", 21 | "DAW" 22 | ], 23 | "author": "Rikard Lindstrom", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/rikard-io/SampleEditorView/issues" 27 | }, 28 | "homepage": "https://github.com/rikard-io/SampleEditorView", 29 | "devDependencies": { 30 | "babel-cli": "^6.26.0", 31 | "babel-core": "^6.26.0", 32 | "babel-eslint": "^8.0.3", 33 | "babel-loader": "^7.1.2", 34 | "babel-plugin-add-module-exports": "^0.2.1", 35 | "babel-preset-env": "^1.6.1", 36 | "babel-preset-stage-2": "^6.24.1", 37 | "chai": "^4.1.2", 38 | "eslint": "^4.13.1", 39 | "eslint-loader": "^1.9.0", 40 | "karma": "^2.0.0", 41 | "karma-chrome-launcher": "^2.2.0", 42 | "karma-mocha": "^1.3.0", 43 | "karma-webpack": "^2.0.9", 44 | "mocha": "^4.0.1", 45 | "webpack": "^3.10.0", 46 | "webpack-dev-server": "^2.11.1", 47 | "yargs": "^10.0.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Ruler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Rikard Lindstrom 3 | * @Filename: Ruler.js 4 | */ 5 | 6 | import CanvasUI from './CanvasUI'; 7 | 8 | const defaultProperties = { 9 | vZoom: 1, 10 | offset: 0, 11 | background: '#AAA', 12 | color: '#222', 13 | interval: 'auto', 14 | unit: 's', 15 | duration: 'auto', 16 | quantize: 0, 17 | height: 40 18 | }; 19 | 20 | class Ruler extends CanvasUI { 21 | 22 | constructor(props) { 23 | 24 | super(defaultProperties, props); 25 | 26 | } 27 | 28 | render() { 29 | 30 | let ctx = this.ctx; 31 | 32 | let w = this.props.width; 33 | let h = this.props.height; 34 | 35 | ctx.fillStyle = this.props.background; 36 | ctx.fillRect(0, 0, w, h); 37 | ctx.strokeStyle = ctx.fillStyle = this.props.color; 38 | ctx.lineWidth = 0.5; 39 | 40 | let displayDuration = this.displayDuration; 41 | let secondsPerPixel = displayDuration / w; 42 | 43 | let interval = this.props.interval; 44 | 45 | if (interval === 'auto') { 46 | interval = this.quantizeRuler(displayDuration); 47 | } 48 | 49 | interval = Math.max(0.001, interval); 50 | 51 | let precision = Math.max(2, Math.min(3, Math.round(1 / interval))); 52 | let pixelsPerInterval = (1 / secondsPerPixel) * interval; 53 | let drawPoints = w / pixelsPerInterval; 54 | 55 | let markerInterval = 5;// Math.max(1, Math.round(interval * 4)); 56 | 57 | let x = -((this.props.offset / interval) % markerInterval) * 58 | pixelsPerInterval; 59 | 60 | let startTime = this.props.offset; 61 | 62 | for (let i = 0; i < drawPoints + markerInterval; i++) { 63 | let isMarker = i % markerInterval === 0; 64 | 65 | ctx.beginPath(); 66 | if (isMarker) { 67 | ctx.moveTo(x, 0); 68 | ctx.lineTo(x, h); 69 | } else { 70 | ctx.moveTo(x, h); 71 | ctx.lineTo(x, h * 0.63); 72 | } 73 | ctx.stroke(); 74 | 75 | if (isMarker) { 76 | let fontSize = this.props.width / 71; 77 | 78 | ctx.font = `${fontSize}px Arial`; 79 | ctx.fillText((startTime + (x / pixelsPerInterval) * 80 | interval).toFixed(precision) + this.props.unit, x + 5, fontSize); 81 | } 82 | 83 | x += pixelsPerInterval; 84 | 85 | } 86 | 87 | return this; 88 | 89 | } 90 | 91 | quantizeRuler(d) { 92 | const MAX_PIXEL_W = 20; 93 | const MIN_PIXEL_W = 60; 94 | 95 | let pixelsPerSecond = this.props.width / d; 96 | let r = 5 / pixelsPerSecond; 97 | let oct = -Math.floor(Math.log(r) / Math.log(10) + 1); 98 | let dec = (Math.pow(10, oct)); 99 | 100 | let q; 101 | 102 | if (!this.props.quantize) { 103 | let c = [1, 2, 5][Math.round(r * dec * 2)]; 104 | 105 | q = c / dec; 106 | 107 | } else { 108 | q = this.props.quantize; 109 | while (q * pixelsPerSecond < MAX_PIXEL_W) q += this.props.quantize; 110 | while (q * pixelsPerSecond > MIN_PIXEL_W) q /= 5; 111 | 112 | } 113 | return q; 114 | } 115 | 116 | } 117 | 118 | export default Ruler; 119 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SampleEditorView 6 | 58 | 59 | 60 |

Example

61 | 62 |
63 |
64 |
Alt+Mouse to Zoom/Pan
65 | 66 | 67 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/eventMixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple event $triggering and listening mixin, somewhat borrowed from Backbone 3 | * @author Rikard Lindstrom 4 | * @mixin 5 | */ 6 | 'use strict'; 7 | 8 | export default { 9 | 10 | /** 11 | * Register a listener 12 | */ 13 | $on: function (name, callback, context) { 14 | /** @member */ 15 | this._events = this._events || {}; 16 | 17 | let events = this._events[ name ] || (this._events[ name ] = []); 18 | 19 | events.push({ 20 | callback: callback, 21 | ctxArg: context, 22 | context: context || this 23 | }); 24 | 25 | return this; 26 | }, 27 | 28 | /** 29 | * Unregister a listener 30 | */ 31 | $off: function (name, callback, context) { 32 | let i, len, listener, retain; 33 | 34 | if (!this._events || !this._events[ name ]) { 35 | return this; 36 | } 37 | 38 | if (!name && !callback && !context) { 39 | this._events = {}; 40 | } 41 | 42 | let eventListeners = this._events[ name ]; 43 | 44 | if (eventListeners) { 45 | retain = []; 46 | // silly redundancy optimization, might be better to keep it DRY 47 | if (callback && context) { 48 | for (i = 0, len = eventListeners.length; i < len; i++) { 49 | listener = eventListeners[ i ]; 50 | if (callback !== listener.callback && context !== listener.ctxArg) { 51 | retain.push(eventListeners[ i ]); 52 | } 53 | } 54 | } else if (callback) { 55 | for (i = 0, len = eventListeners.length; i < len; i++) { 56 | listener = eventListeners[ i ]; 57 | if (callback !== listener.callback) { 58 | retain.push(eventListeners[ i ]); 59 | } 60 | } 61 | } else if (context) { 62 | for (i = 0, len = eventListeners.length; i < len; i++) { 63 | listener = eventListeners[ i ]; 64 | if (context !== listener.ctxArg) { 65 | retain.push(eventListeners[ i ]); 66 | } 67 | } 68 | } 69 | 70 | this._events[ name ] = retain; 71 | } else if (context || callback) { 72 | Object.keys(this._events).forEach((k) => { 73 | this.$off(k, callback, context); 74 | }); 75 | } 76 | 77 | if (!this._events[ name ].length) { 78 | delete this._events[ name ]; 79 | } 80 | 81 | return this; 82 | }, 83 | 84 | /** 85 | * $trigger an event 86 | */ 87 | $trigger: function (name) { 88 | if (!this._events || !this._events[ name ]) { 89 | return this; 90 | } 91 | 92 | let i, args, binding, listeners; 93 | 94 | listeners = this._events[ name ]; 95 | 96 | args = [].splice.call(arguments, 1); 97 | for (i = listeners.length - 1; i >= 0; i--) { 98 | binding = listeners[ i ]; 99 | binding.callback.apply(binding.context, args); 100 | } 101 | 102 | return this; 103 | }, 104 | 105 | /** 106 | * Triggered on the next heartbeat to avoid mass triggering 107 | * when changing multiple props at once 108 | */ 109 | $triggerDeferred(event) { 110 | this._changeBatchTimers = this._changeBatchTimers || {}; 111 | if (this._changeBatchTimers[ event ] == null) { 112 | this._changeBatchTimers[ event ] = setTimeout(()=>{ 113 | this.$trigger(event); 114 | this._changeBatchTimers[ event ] = null; 115 | }, 50); 116 | } 117 | } 118 | 119 | }; 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Travis](https://travis-ci.org/rikard-io/SampleEditorView.svg?branch=master) 2 | 3 | # WebAudio SampleEditorView 4 | 5 | Audio / Waveform UI for displaying WebAudio AudioBuffers with zoom, select and looping markers interaction. Much like what you would find in a DAW. 6 | 7 | ![Alt text](/screenshot.gif?raw=true "Screenshot") 8 | 9 | ## Warning 10 | 11 | This repo is in it's early stage. May not be appropriate for production. 12 | 13 | ## Try It 14 | [Live Demo](https://rawgit.com/rikard-io/SampleEditorView/master/index.html) 15 | 16 | ## Install 17 | 18 | `npm i -D sample-editor-view` or `yarn add -D sample-editor-view` 19 | 20 | ## Features 21 | 22 | * Adjustable detail - Fast rendering when precision is not important 23 | * No external dependencies 24 | * Should work wherever WebAudio works 25 | * Canvas based with customizable rendering size (for retina) 26 | * Modular approach. Easy to use just the Waveform for example 27 | * Quantize selection and looping points to a time interval 28 | 29 | ## Alternatives 30 | 31 | * https://github.com/bbc/waveform-data.js 32 | Probably better performance-wise, but much bigger in scope. Also missing some more DAW-like features I required. 33 | 34 | ## Usage 35 | 36 | ### Create / Init: 37 | ``` 38 | let editor = new SampleEditorView({...}) 39 | 40 | // ... load / create buffer 41 | 42 | editor.buffer = buffer 43 | ``` 44 | 45 | ### options 46 | 47 | All options can be sent to the constructor in an object, or updated / read later 48 | with: `editor.props.[propName] = x`. 49 | Setting a property will also cause a rerendering of the updated part. 50 | 51 | | Property | Default | Description | 52 | | --------------- |:-------:|----------------------------------------:| 53 | | hZoom  | 1 | Horizontal zoom level. In ratio where 1 shows the full duration and 2 show half the duration. | 54 | | vZoom  | 2 | Vertical zoom level. A multiplier used when drawing the hight of the waveform. | 55 | | offset | 0 | Offset in seconds from where to start reading from the buffer. | 56 | | background | '#ddd' | Background color. | 57 | | color | '#222' | Foreground color (text and waveform). | 58 | | selectBackground | '#222' | Background select color. | 59 | | selectColor | '#ddd' | Foreground select color (waveform). | 60 | | width | 640 | Width in pixels. Multiply with window.devicePixelRatio and adjust CSS to adopt to retina / high-res. | 61 | | height | 320 | Height in pixels. Multiply with window.devicePixelRatio and adjust CSS to adopt to retina / high-res. | 62 | | channel | 0 | What buffer-channel to read from. | 63 | | resolution | 1 | Level of detail, should be kept between 1-N, where 1 means to draw every pixel and N every N:th pixel | 64 | | startPosition | 0 | Position of start marker | 65 | | uiZoomStickiness | 0.1 | When panning / zooming, this is used to prevent accidental zooming (when panning). | 66 | | duration | 'auto' | What duration to use to determine max pan / offset etc. 'auto' means using the buffer duration. | 67 | | loop | true | Show or hide loop markers. | 68 | | loopStart | 0 | Start position for loop markers, in seconds. | 69 | | loopEnd | 1 | End position for loop markers, in seconds. | 70 | | selectStart | 0 | Start position for selection, in seconds. | 71 | | selectEnd | 0 | End position for selection, in seconds. | 72 | | quantize | 0.0125 | Snapping or quantize duration in seconds. 0.125 would snap to 1/16th note in 120 bpm. | 73 | | buffer | null | What AudioBuffer to read from. | 74 | 75 | ## Developing 76 | 77 | * Run any dev server or the file system 78 | * `yarn build` or `npm run build` - produces production version of SampleEditorView under the `dist` folder 79 | * `yarn dev` or `npm run dev` - produces development version SampleEditorView and runs a watcher 80 | * `yarn test` or `npm run test` - runs the to-do-tests :) 81 | * `yarn test:watch` or `npm run test:watch` - same as above but in a watch mode 82 | 83 | ## License 84 | 85 | * See LICENSE 86 | -------------------------------------------------------------------------------- /src/Props.js: -------------------------------------------------------------------------------- 1 | /** 2 | * "Magic" Properties with bindings to react to changes 3 | * @Author: Rikard Lindstrom 4 | * @Filename: Props.js 5 | */ 6 | 7 | import eventMixin from './eventMixin'; 8 | 9 | class Props { 10 | 11 | /** 12 | * @constructor 13 | * @param {Object[]]} props - Properties that will me merged right to left. 14 | */ 15 | constructor(...props) { 16 | 17 | props = props.filter(p => !!p); 18 | 19 | let propsObject = props.reduce((a, p) => { return Object.assign(a, p); }, {}); 20 | 21 | Object.keys(propsObject).forEach(k => { 22 | Object.defineProperty(this, k, { 23 | get() { 24 | return propsObject[ k ]; 25 | }, 26 | set(v) { 27 | let oldV = propsObject[ k ]; 28 | 29 | propsObject[ k ] = v; 30 | this.$triggerDeferred('defered_change'); 31 | this.$trigger('change', k, v); 32 | this.$trigger(`change:${k}`, v, oldV); 33 | } 34 | }); 35 | }); 36 | 37 | this.$privateProps = propsObject; 38 | this.$keys = Object.keys(propsObject); 39 | } 40 | 41 | /** 42 | * Observe a value for changes, much like .$on but also initializes the callback 43 | * with current value 44 | * @param {string|string[]]} key - Key or keys to observe 45 | * @param {function} cb - Callback 46 | * @param {Object} ctx - Context for callback 47 | */ 48 | $observe(key, cb, ctx = this) { 49 | if (Array.isArray(key)) { 50 | key.forEach(k => this.$observe(k, cb, ctx)); 51 | return this; 52 | } 53 | if (this.$privateProps[ key ] === undefined) throw new Error('Can\' observe undefined prop ' + key); 54 | cb.call(ctx, this.$privateProps[ key ]); 55 | this.$on(`change:${key}`, cb, ctx); 56 | return this; 57 | } 58 | 59 | /** 60 | * Unobserve a value for changes 61 | * @param {string|string[]]} key - Key or keys to observe 62 | * @param {function} cb - Callback 63 | * @param {Object} ctx - Context for callback 64 | */ 65 | $unobserve(key, cb, ctx) { 66 | if (Array.isArray(key)) { 67 | key.forEach(k => this.$unobserve(k, cb, ctx)); 68 | return this; 69 | } 70 | this.$off(`change:${key}`, cb, ctx); 71 | return this; 72 | } 73 | 74 | /** 75 | * Wrapper for $observe, sets the properties on the target instead of using a callback 76 | * @param {Object} target - Target context 77 | * @param {string|string[]]} key - Key or keys to observe 78 | */ 79 | $bind(target, ...keys) { 80 | keys.forEach(k => { 81 | this.$observe(k, function (v) { 82 | target[ k ] = v; 83 | }); 84 | }); 85 | 86 | return this; 87 | } 88 | 89 | /** 90 | * Wrapper for $unobserve 91 | * @param {Object} target - Target context 92 | * @param {string|string[]]} key - Key or keys to observe 93 | */ 94 | $unbind(target, ...keys) { 95 | if (keys && keys.length) { 96 | keys.forEach(k => { this.$unobserve(k, null, target); }); 97 | } else { 98 | this.$off(null, null, target); 99 | } 100 | 101 | return this; 102 | } 103 | 104 | /** 105 | * Reversed $bind:ing between Props objects with filters. 106 | * @param {Props} otherProps - Another instance of Props 107 | * @param {Object} map - Mapping specification in the form of {propName: filterFn} 108 | */ 109 | $map(otherProps, map) { 110 | Object.keys(map).forEach(k => { 111 | otherProps.$observe(k, v => { 112 | this[ k ] = map[ k ](v); 113 | }); 114 | }); 115 | 116 | return this; 117 | } 118 | 119 | /** 120 | * Reversed $bind:ing between Props objects. 121 | * @param {Props} otherProps - Another instance of Props 122 | * @param {string[]]} keys - Keys to observe 123 | */ 124 | $link(otherProps, keys = null) { 125 | keys = keys || Object.keys(this.$privateProps).filter(k => otherProps.$privateProps[ k ] !== undefined); 126 | otherProps.$bind(this, ...keys); 127 | return this; 128 | } 129 | 130 | /** 131 | * Reversed $unbind:ing between Props objects with filters. 132 | * @param {Props} otherProps - Another instance of Props 133 | * @param {string[]]} keys - Keys to stop observing 134 | */ 135 | $unlink(otherProps, keys = null) { 136 | keys = keys || Object.keys(this.$privateProps).filter(k => otherProps.$privateProps[ k ] !== undefined); 137 | otherProps.$unbind(this, ...Object.keys(this.$privateProps)); 138 | return this; 139 | } 140 | } 141 | 142 | Object.assign(Props.prototype, eventMixin); 143 | 144 | export default Props; 145 | -------------------------------------------------------------------------------- /src/Waveform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: Rikard Lindstrom 3 | * @Filename: Waveform.js 4 | */ 5 | 6 | import CanvasUI from './CanvasUI'; 7 | 8 | const defaultProperties = { 9 | vZoom: 1, 10 | offset: 0, 11 | background: '#ddd', 12 | color: '#222', 13 | selectColor: '#ddd', 14 | selectBackground: '#222', 15 | width: 640, 16 | height: 320, 17 | selectStart: 0, 18 | selectEnd: 0, 19 | channel: 0, 20 | resolution: 1 21 | }; 22 | 23 | function avarage(data, from, to, interval = 1, abs = false, q) { 24 | 25 | interval = Math.max(1, interval); 26 | to = Math.min(data.length, to); 27 | to = Math.min(from + 100, to); 28 | 29 | let tot = 0; 30 | 31 | for (let i = from; i < to; i += interval) { 32 | tot += abs ? Math.abs(data[ i ]) : data[ i ]; 33 | 34 | } 35 | let len = to - from; 36 | let avg = (tot / len) * interval; 37 | 38 | return avg; 39 | 40 | } 41 | 42 | class Waveform extends CanvasUI { 43 | 44 | constructor(props) { 45 | 46 | super(defaultProperties, props); 47 | 48 | } 49 | 50 | render() { 51 | 52 | let buffer = this.props.buffer; 53 | 54 | if (!buffer) return this; 55 | 56 | let w = this.props.width; 57 | let h = this.props.height; 58 | 59 | let ctx = this.ctx; 60 | 61 | ctx.fillStyle = this.props.background; 62 | ctx.fillRect(0, 0, w, h); 63 | 64 | let data = buffer.getChannelData(this.props.channel); 65 | let displayLength = data.length / this.props.hZoom; 66 | let pointsToDraw = Math.min(w / this.props.resolution, displayLength); 67 | let bufferStepLen = displayLength / pointsToDraw ; 68 | let pointDistance = w / pointsToDraw ; 69 | 70 | let halfHeight = h / 2; 71 | 72 | let offsetSamples = this.props.offset * buffer.sampleRate; 73 | 74 | ctx.fillStyle = this.props.color; 75 | 76 | let drawSymetric = bufferStepLen > pointDistance; 77 | 78 | ctx.beginPath(); 79 | ctx.moveTo(0, halfHeight); 80 | ctx.lineTo(w, halfHeight); 81 | ctx.stroke(); 82 | 83 | ctx.moveTo(0, halfHeight); 84 | 85 | // internal render function to be able to divide the rendering on selection 86 | let draw = (from, to, color, background) => { 87 | 88 | from = Math.max(0, Math.min(from, pointsToDraw)); 89 | to = Math.max(0, Math.min(to, pointsToDraw)); 90 | 91 | let lastDataIndex = 0; 92 | let pointsDrawn = []; 93 | 94 | ctx.beginPath(); 95 | 96 | let x = from * pointDistance; 97 | 98 | if (background) { 99 | let len = (to - from) * pointDistance; 100 | 101 | ctx.fillStyle = background; 102 | ctx.fillRect(x, 0, len, h); 103 | } 104 | 105 | for (let i = from; i < to; i++) { 106 | 107 | let j = Math.floor((i + 1) * bufferStepLen + offsetSamples); 108 | 109 | if (drawSymetric) { 110 | // avarage with abs 111 | let v = j >= 0 ? avarage(data, lastDataIndex, j, 1, true) : 0; 112 | 113 | if (v >= 0) { 114 | let y = v * this.props.vZoom * halfHeight; 115 | 116 | if (i === from) { 117 | ctx.moveTo(x, halfHeight - y); 118 | } 119 | ctx.lineTo(x, halfHeight - y); 120 | pointsDrawn.push(x, y); 121 | } 122 | 123 | } else { 124 | // avarage without abs 125 | let v = j >= 0 ? avarage(data, lastDataIndex, j, 1, false) : 0; 126 | let y = v * this.props.vZoom * halfHeight; 127 | 128 | if (i === from) { 129 | ctx.moveTo(0, halfHeight - y); 130 | } 131 | ctx.lineTo(x, halfHeight - y); 132 | 133 | } 134 | 135 | x += pointDistance; 136 | 137 | lastDataIndex = j; 138 | } 139 | 140 | // fill in the flip side if we should do a symetrical waveform 141 | if (drawSymetric) { 142 | for (let i = pointsDrawn.length - 1; i > 0; i -= 2) { 143 | let x = pointsDrawn[ i - 1 ]; 144 | let y = pointsDrawn[ i ]; 145 | 146 | ctx.lineTo(x, halfHeight + y); 147 | } 148 | 149 | ctx.fillStyle = color; 150 | ctx.fill(); 151 | 152 | } else { 153 | ctx.strokeStyle = color; 154 | ctx.stroke(); 155 | } 156 | 157 | }; 158 | 159 | if (this.props.selectStart !== this.props.selectEnd) { 160 | let timePerPoint = (this.duration / this.props.hZoom) / pointsToDraw ; 161 | let relStartTime = this.props.selectStart - this.props.offset; 162 | let startPoint = Math.floor(relStartTime / timePerPoint); 163 | let relEndtTime = this.props.selectEnd - this.props.offset; 164 | let endPoint = Math.floor(relEndtTime / timePerPoint); 165 | 166 | if (endPoint > 0 && startPoint < pointsToDraw) { 167 | if (startPoint > 0) { 168 | draw(0, startPoint, this.props.color, this.props.background); 169 | } 170 | 171 | draw(Math.max(0, startPoint), Math.min(pointsToDraw, endPoint), 172 | this.props.selectColor, this.props.selectBackground); 173 | 174 | if (endPoint < pointsToDraw) { 175 | draw(endPoint, pointsToDraw, this.props.color, this.props.background); 176 | } 177 | } else { 178 | draw(0, pointsToDraw, this.props.color, this.props.background); 179 | } 180 | 181 | } else { 182 | draw(0, pointsToDraw, this.props.color, this.props.background); 183 | } 184 | 185 | return this; 186 | 187 | } 188 | 189 | } 190 | 191 | export default Waveform; 192 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | 9 | "globals": { 10 | "document": false, 11 | "escape": false, 12 | "navigator": false, 13 | "unescape": false, 14 | "window": false, 15 | "describe": true, 16 | "before": true, 17 | "it": true, 18 | "expect": true, 19 | "sinon": true 20 | }, 21 | 22 | "parser": "babel-eslint", 23 | 24 | "plugins": [ 25 | 26 | ], 27 | 28 | "rules": { 29 | "block-scoped-var": 1, 30 | "brace-style": [1, "1tbs", { "allowSingleLine": true }], 31 | "camelcase": [1, { "properties": "always" }], 32 | "comma-dangle": [1, "never"], 33 | "comma-spacing": [1, { "before": false, "after": true }], 34 | "comma-style": [1, "last"], 35 | "complexity": 0, 36 | "consistent-return": 1, 37 | "consistent-this": 0, 38 | "curly": [1, "multi-line"], 39 | "default-case": 0, 40 | "dot-location": [1, "property"], 41 | "dot-notation": 0, 42 | "eol-last": 1, 43 | "eqeqeq": [1, "allow-null"], 44 | "func-names": 0, 45 | "func-style": 0, 46 | "generator-star-spacing": [1, "both"], 47 | "guard-for-in": 0, 48 | "handle-callback-err": [1, "^(err|error|anySpecificError)$" ], 49 | "indent": [1, 2, { "SwitchCase": 1 }], 50 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }], 51 | "keyword-spacing": [1, {"before": true, "after": true}], 52 | "linebreak-style": 0, 53 | "max-depth": 0, 54 | "max-len": [1, 120, 4], 55 | "max-nested-callbacks": 0, 56 | "max-params": 0, 57 | "max-statements": 0, 58 | "new-cap": [1, { "newIsCap": true, "capIsNew": false }], 59 | "newline-after-var": [1, "always"], 60 | "new-parens": 1, 61 | "no-alert": 0, 62 | "no-array-constructor": 1, 63 | "no-bitwise": 0, 64 | "no-caller": 1, 65 | "no-catch-shadow": 0, 66 | "no-cond-assign": 1, 67 | "no-console": 0, 68 | "no-constant-condition": 0, 69 | "no-continue": 0, 70 | "no-control-regex": 1, 71 | "no-debugger": 1, 72 | "no-delete-var": 1, 73 | "no-div-regex": 0, 74 | "no-dupe-args": 1, 75 | "no-dupe-keys": 1, 76 | "no-duplicate-case": 1, 77 | "no-else-return": 1, 78 | "no-empty": 0, 79 | "no-empty-character-class": 1, 80 | "no-eq-null": 0, 81 | "no-eval": 1, 82 | "no-ex-assign": 1, 83 | "no-extend-native": 1, 84 | "no-extra-bind": 1, 85 | "no-extra-boolean-cast": 1, 86 | "no-extra-parens": 0, 87 | "no-extra-semi": 0, 88 | "no-extra-strict": 0, 89 | "no-fallthrough": 1, 90 | "no-floating-decimal": 1, 91 | "no-func-assign": 1, 92 | "no-implied-eval": 1, 93 | "no-inline-comments": 0, 94 | "no-inner-declarations": [1, "functions"], 95 | "no-invalid-regexp": 1, 96 | "no-irregular-whitespace": 1, 97 | "no-iterator": 1, 98 | "no-label-var": 1, 99 | "no-labels": 1, 100 | "no-lone-blocks": 0, 101 | "no-lonely-if": 0, 102 | "no-loop-func": 0, 103 | "no-mixed-requires": 0, 104 | "no-mixed-spaces-and-tabs": [1, false], 105 | "no-multi-spaces": 1, 106 | "no-multi-str": 1, 107 | "no-multiple-empty-lines": [1, { "max": 1 }], 108 | "no-native-reassign": 1, 109 | "no-negated-in-lhs": 1, 110 | "no-nested-ternary": 0, 111 | "no-new": 1, 112 | "no-new-func": 1, 113 | "no-new-object": 1, 114 | "no-new-require": 1, 115 | "no-new-wrappers": 1, 116 | "no-obj-calls": 1, 117 | "no-octal": 1, 118 | "no-octal-escape": 1, 119 | "no-path-concat": 0, 120 | "no-plusplus": 0, 121 | "no-process-env": 0, 122 | "no-process-exit": 0, 123 | "no-proto": 1, 124 | "no-redeclare": 1, 125 | "no-regex-spaces": 1, 126 | "no-reserved-keys": 0, 127 | "no-restricted-modules": 0, 128 | "no-return-assign": 1, 129 | "no-script-url": 0, 130 | "no-self-compare": 1, 131 | "no-sequences": 1, 132 | "no-shadow": 0, 133 | "no-shadow-restricted-names": 1, 134 | "no-spaced-func": 1, 135 | "no-sparse-arrays": 1, 136 | "no-sync": 0, 137 | "no-ternary": 0, 138 | "no-throw-literal": 1, 139 | "no-trailing-spaces": 1, 140 | "no-undef": 1, 141 | "no-undef-init": 1, 142 | "no-undefined": 0, 143 | "no-underscore-dangle": 0, 144 | "no-unneeded-ternary": 1, 145 | "no-unreachable": 1, 146 | "no-unused-expressions": 0, 147 | "no-unused-vars": [1, { "vars": "all", "args": "none" }], 148 | "no-use-before-define": 1, 149 | "no-var": 0, 150 | "no-void": 0, 151 | "no-warning-comments": 0, 152 | "no-with": 1, 153 | "one-var": 0, 154 | "operator-assignment": 0, 155 | "operator-linebreak": [1, "after"], 156 | "padded-blocks": 0, 157 | "quote-props": 0, 158 | "quotes": [1, "single", "avoid-escape"], 159 | "radix": 1, 160 | "semi": [1, "always"], 161 | "semi-spacing": 0, 162 | "sort-vars": 0, 163 | "space-before-blocks": [1, "always"], 164 | "space-before-function-paren": [1, {"anonymous": "always", "named": "never"}], 165 | "space-in-brackets": 0, 166 | "space-in-parens": [1, "never"], 167 | "space-infix-ops": 1, 168 | "space-unary-ops": [1, { "words": true, "nonwords": false }], 169 | "spaced-comment": [1, "always"], 170 | "strict": 0, 171 | "use-isnan": 1, 172 | "valid-jsdoc": 0, 173 | "valid-typeof": 1, 174 | "vars-on-top": 1, 175 | "wrap-iife": [1, "any"], 176 | "wrap-regex": 0, 177 | "yoda": [1, "never"] 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/SampleEditorView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main program, compiles everything onto one canvas and handles interaction 3 | * @Author: Rikard Lindstrom 4 | * @Filename: SampleEditorView.js 5 | */ 6 | 7 | import CanvasUI from './CanvasUI'; 8 | import Waveform from './Waveform'; 9 | import Ruler from './Ruler'; 10 | import LineMarker from './LineMarker'; 11 | import LoopMarker from './LoopMarker'; 12 | 13 | const defaultProperties = { 14 | hZoom: 1, 15 | vZoom: 2, 16 | offset: 0, 17 | background: '#ddd', 18 | color: '#222', 19 | selectColor: '#ddd', 20 | selectBackground: '#222', 21 | width: 640, 22 | height: 320, 23 | channel: 0, 24 | resolution: 1, 25 | startPosition: 42.1, 26 | uiZoomStickiness: 0.1, 27 | duration: 'auto', 28 | visible: true, 29 | loop: true, 30 | loopStart: 0, 31 | loopEnd: 1, 32 | selectStart: 0, 33 | selectEnd: 0, 34 | quantize: 0.0125, 35 | buffer: null 36 | }; 37 | 38 | class SampleEditorView extends CanvasUI { 39 | 40 | constructor(props) { 41 | 42 | super(defaultProperties, props); 43 | 44 | this.waveForm = new Waveform({}); 45 | this.ruler = new Ruler({}); 46 | this.startMarker = new LineMarker({}); 47 | this.zoomMarker = new LineMarker({visible: false}); 48 | this.loopLengthMarker = new LoopMarker({visible: true}); 49 | this.loopStartMarker = new LineMarker({visible: true}); 50 | this.loopEndMarker = new LineMarker({dir: -1, visible: true}); 51 | 52 | this.waveForm.props.$link(this.props); 53 | this.ruler.props.$link(this.props, ['hZoom', 'width', 'offset', 'quantize', 54 | 'buffer' ]).$map(this.props, { height: v => v / 16 }); 55 | this.zoomMarker.props.$link(this.props, ['height']); 56 | this.loopEndMarker.props.$link(this.props, ['height']) 57 | .$map(this.props, { width: v => v / 64 }); 58 | this.loopStartMarker.props.$link(this.props, ['height']) 59 | .$map(this.props, { width: v => v / 64 }); 60 | this.loopLengthMarker.props.$map(this.props, { 61 | height: v => v / 32 62 | }); 63 | 64 | this.startMarker.props.$link(this.props, ['height']) 65 | .$map(this.props, { width: v => v / 64 }); 66 | 67 | this.render = this.render.bind(this); 68 | 69 | this._setupUI(); 70 | 71 | this.props.$on('defered_change', this.renderIfDirty, this); 72 | 73 | this.zoomMarker.props.$observe('visible', this.renderIfDirty, this); 74 | 75 | this.canvas.classList.add('SampleEditorView'); 76 | } 77 | 78 | render() { 79 | let ctx = this.ctx; 80 | 81 | ctx.drawImage(this.waveForm.renderIfDirty().canvas, 0, 0); 82 | ctx.drawImage(this.zoomMarker.renderIfDirty().canvas, 83 | this.zoomMarker.position, 0); 84 | 85 | ctx.drawImage(this.ruler.renderIfDirty().canvas, 0, 0); 86 | 87 | ctx.drawImage(this.startMarker.renderIfDirty().canvas, 88 | this._timeToPixel(this.props.startPosition), 10); 89 | 90 | this.loopLengthMarker.props.width = this._timeToPixel(this.props.loopEnd) - 91 | this._timeToPixel(this.props.loopStart); 92 | 93 | if (this.loopLengthMarker.props.width > 0) { 94 | ctx.drawImage(this.loopLengthMarker.renderIfDirty().canvas, 95 | this._timeToPixel(this.props.loopStart), 20); 96 | } 97 | ctx.drawImage(this.loopStartMarker.renderIfDirty().canvas, 98 | this._timeToPixel(this.props.loopStart), 20); 99 | ctx.drawImage(this.loopEndMarker.renderIfDirty().canvas, 100 | this._timeToPixel(this.props.loopEnd) - this.loopEndMarker.props.width, 20); 101 | 102 | } 103 | 104 | _timeToPixel(time) { 105 | time -= this.props.offset; 106 | if (time === 0 || this.displayDuration === 0) { 107 | return 1; 108 | } 109 | let px = (time / this.displayDuration) * this.props.width; 110 | 111 | return Math.max(1, Math.round(px)); 112 | } 113 | 114 | _pixelToTime(pixel) { 115 | return (pixel / this.props.width) * this.displayDuration; 116 | } 117 | 118 | _getLoopRect() { 119 | return { 120 | x1: this._timeToPixel(this.props.loopStart), 121 | y1: 20, 122 | x2: this._timeToPixel(this.props.loopEnd), 123 | y2: this.props.height - 20 124 | }; 125 | } 126 | 127 | // a pretty crude hittest to find target from a relative mouse position 128 | _hitTest(point) { 129 | 130 | if (point.y < this.ruler.props.height) { 131 | 132 | if (this.props.loop) { 133 | let loopRect = this._getLoopRect(); 134 | 135 | if (point.y >= loopRect.y1 && point.y < loopRect.y2) { 136 | if (Math.abs(point.x - loopRect.x1) < 5) { 137 | return this.loopStartMarker; 138 | } else if (Math.abs(point.x - loopRect.x2) < 5) { 139 | return this.loopEndMarker; 140 | } else if (point.x >= loopRect.x1 && point.x < loopRect.x2) { 141 | return this.loopLengthMarker; 142 | } 143 | } 144 | } 145 | 146 | if (point.x) {return this.ruler;} 147 | } 148 | return this.waveForm; 149 | } 150 | 151 | // update mouse cursor to reflect active target 152 | _updateCursor(hitTarget, e) { 153 | if (hitTarget === this.ruler) { 154 | this.canvas.style.cursor = 'pointer'; 155 | } else if (hitTarget === this.loopStartMarker) { 156 | this.canvas.style.cursor = 'e-resize'; 157 | } else if (hitTarget === this.loopEndMarker) { 158 | this.canvas.style.cursor = 'w-resize'; 159 | } else if (hitTarget === this.loopLengthMarker) { 160 | this.canvas.style.cursor = 'move'; 161 | } else if (hitTarget === this.waveForm && e.altKey) { 162 | this.canvas.style.cursor = 'zoom-in'; 163 | } else { 164 | this.canvas.style.cursor = 'auto'; 165 | } 166 | } 167 | 168 | // almost fully self contained ui interaction 169 | _setupUI() { 170 | let mouseDown = false; 171 | let lastY = 0; 172 | let zoomThresh = 0; 173 | let canvasTarget = null; 174 | let doZoom = false; 175 | let lastMousePos = { x: 0, y: 0 }; 176 | let startXTime = 0; 177 | 178 | const toRelativeMovement = (e) => { 179 | let rect = this.canvas.getBoundingClientRect(); 180 | let pixelRatio = this.pixelRatio; 181 | 182 | let x = (e.pageX - rect.left) * pixelRatio.x; 183 | let y = (e.pageY - rect.top) * pixelRatio.y; 184 | 185 | let movementX = e.movementX ? e.movementX * pixelRatio.x : 0; 186 | let movementY = e.movementY ? e.movementY * pixelRatio.y : 0; 187 | 188 | return { 189 | rect, 190 | x, 191 | y, 192 | movementX, 193 | movementY 194 | }; 195 | }; 196 | 197 | const quantizePosition = ({ x, y }) => { 198 | 199 | if (!this.props.quantize) return { x, y }; 200 | 201 | let offsetPx = (this.props.offset / this.displayDuration) * this.props.width; 202 | let pxQuant = (this.props.quantize / this.displayDuration) * this.props.width; 203 | 204 | x += offsetPx; 205 | x = Math.round(x / pxQuant) * pxQuant; 206 | x -= offsetPx; 207 | return { x, y }; 208 | }; 209 | 210 | this.canvas.addEventListener('mousedown', (e)=>{ 211 | mouseDown = true; 212 | lastY = null; 213 | zoomThresh = 0; 214 | doZoom = false; 215 | 216 | let pos = toRelativeMovement(e); 217 | 218 | canvasTarget = this._hitTest(pos); 219 | 220 | this._updateCursor(canvasTarget, e); 221 | 222 | if (canvasTarget === this.waveForm) { 223 | if (e.altKey) { 224 | doZoom = true; 225 | this.zoomMarker.position = pos.x; 226 | this.zoomMarker.props.visible = true; 227 | this.canvas.requestPointerLock(); 228 | } 229 | } 230 | if (!doZoom) { 231 | pos = quantizePosition(pos); 232 | } 233 | startXTime = this._pixelToTime(pos.x) + this.props.offset; 234 | !doZoom && (this.props.selectEnd = this.props.selectStart = startXTime); 235 | lastMousePos = pos; 236 | 237 | }); 238 | 239 | document.addEventListener('mouseup', ()=>{ 240 | mouseDown = false; 241 | if (doZoom) { 242 | this.zoomMarker.props.visible = false; 243 | document.exitPointerLock(); 244 | doZoom = false; 245 | } 246 | }); 247 | 248 | document.addEventListener('mousemove', (e)=>{ 249 | 250 | if (mouseDown) { 251 | 252 | let { x, y, movementX, movementY, rect } = toRelativeMovement(e); 253 | let p = { x, y }; 254 | 255 | if (!doZoom) { 256 | 257 | // keep p within boarders of canvas 258 | p.x = Math.max(0, Math.min(this.props.width, p.x)); 259 | p = quantizePosition(p); 260 | 261 | if (canvasTarget === this.ruler) { 262 | this.updateStartPos(p.x); 263 | } 264 | 265 | } 266 | 267 | let deltaX = (doZoom && movementX !== undefined ? movementX : (p.x - lastMousePos.x)) / rect.width; 268 | let deltaY = (doZoom && movementY !== undefined ? movementY : (p.y - lastMousePos.y)) / rect.height; 269 | 270 | let xTime = this._pixelToTime(p.x) + this.props.offset; 271 | let deltaTime = this._pixelToTime(deltaX) * this.props.width / this.pixelRatio.x; 272 | 273 | Object.assign(lastMousePos, p); 274 | 275 | if (doZoom) { 276 | 277 | if (lastY === null) lastY = p.y; 278 | 279 | lastY = p.y; 280 | 281 | zoomThresh += Math.abs(deltaY); 282 | let hZoom = Math.max(1, this.props.hZoom + deltaY * this.props.hZoom); 283 | 284 | if (zoomThresh > this.props.uiZoomStickiness) { 285 | 286 | let zoomDelta = hZoom - this.props.hZoom; 287 | 288 | if (zoomDelta !== 0 && hZoom >= 0.5) { 289 | 290 | let zoomPerc = zoomDelta / this.props.hZoom; 291 | let posRatio = p.x / (rect.width * this.pixelRatio.x); 292 | 293 | this.props.hZoom = hZoom; 294 | this.offset += zoomPerc * posRatio * this.displayDuration; 295 | 296 | } 297 | 298 | } 299 | 300 | this.offset -= (deltaX * 10) / hZoom; 301 | 302 | } else if (canvasTarget === this.waveForm) { 303 | 304 | if (xTime < startXTime) { 305 | this.updateSelection(this.props.selectStart + deltaTime, startXTime); 306 | } else { 307 | this.updateSelection(startXTime, this.props.selectEnd + deltaTime); 308 | } 309 | 310 | } else if (canvasTarget === this.ruler) { 311 | if (p.x >= 0 && p.x < rect.width) { 312 | this.updateStartPos(p.x); 313 | } else { 314 | if (p.x < 0) { 315 | this.offset = Math.max(0, this.props.offset - Math.abs(p.x * 0.1)); 316 | } else { 317 | this.offset = Math.min(this.duration, this.props.offset + (p.x - rect.width) * 0.1); 318 | } 319 | } 320 | } else if (canvasTarget === this.loopLengthMarker) { 321 | 322 | this.updateLoopPos(this.props.loopStart + deltaTime, this.props.loopEnd + deltaTime); 323 | 324 | } else if (canvasTarget === this.loopStartMarker) { 325 | 326 | if (xTime <= this.props.loopEnd) { 327 | this.updateLoopPos(this.props.loopStart + deltaTime, this.props.loopEnd); 328 | } else { 329 | this.updateLoopPos(this.props.loopEnd, this.props.loopEnd); 330 | } 331 | 332 | } else if (canvasTarget === this.loopEndMarker) { 333 | 334 | if (xTime >= this.props.loopStart) { 335 | this.updateLoopPos(this.props.loopStart, this.props.loopEnd + deltaTime); 336 | } else { 337 | this.updateLoopPos(this.props.loopStart, this.props.loopStart); 338 | } 339 | 340 | } 341 | 342 | } else { 343 | this._updateCursor(this._hitTest(toRelativeMovement(e)), e); 344 | } 345 | }); 346 | } 347 | 348 | updateSelection(start, end) { 349 | 350 | start = Math.max(0, start); 351 | end = Math.min(this.duration, end); 352 | 353 | this.props.selectStart = start; 354 | this.props.selectEnd = end; 355 | 356 | } 357 | 358 | updateLoopPos(start, end) { 359 | 360 | if (start < 0) { 361 | let d = Math.abs(start); 362 | 363 | end = Math.min(this.duration, end + d); 364 | start += d; 365 | 366 | } 367 | if (end > this.duration) { 368 | let d = this.duration - end; 369 | 370 | start = Math.max(0, start + d); 371 | end += d; 372 | } 373 | 374 | if (start > end) { 375 | start = end; 376 | } 377 | 378 | if (start >= 0 && end <= this.duration) { 379 | this.props.loopStart = start; 380 | this.props.loopEnd = end; 381 | } 382 | 383 | } 384 | 385 | updateStartPos(px) { 386 | let startPos = ((px / this.canvas.width) * this.duration / this.props.hZoom) + this.props.offset; 387 | 388 | this.props.startPosition = startPos; 389 | } 390 | 391 | get offset() { 392 | return this.props.offset; 393 | } 394 | 395 | set offset(v) { 396 | this.props.offset = Math.max(0, Math.min(this.duration - this.displayDuration, v)); 397 | } 398 | 399 | get buffer() { 400 | return this.props.buffer; 401 | } 402 | 403 | set buffer(buffer) { 404 | this.props.buffer = buffer; 405 | } 406 | 407 | get pixelRatio() { 408 | 409 | let rect = this.canvas.getBoundingClientRect(); 410 | let pixelRatio = { x: this.props.width / rect.width, y: this.props.height / rect.height }; 411 | 412 | return pixelRatio; 413 | 414 | } 415 | } 416 | 417 | export default SampleEditorView; 418 | -------------------------------------------------------------------------------- /dist/SampleEditorView.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("SampleEditorView",[],e):"object"==typeof exports?exports.SampleEditorView=e():t.SampleEditorView=e()}("undefined"!=typeof self?self:this,function(){return function(t){function e(o){if(r[o])return r[o].exports;var n=r[o]={i:o,l:!1,exports:{}};return t[o].call(n.exports,n,n.exports,e),n.l=!0,n.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,o){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=1)}([function(t,e,r){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var n=function(){function t(t,e){for(var r=0;r=e.y1&&t.y=e.x1&&t.xt.props.uiZoomStickiness){var x=w-t.props.hZoom;if(0!==x&&w>=.5){var M=x/t.props.hZoom,P=b.x/(y.width*t.pixelRatio.x);t.props.hZoom=w,t.offset+=M*P*t.displayDuration}}t.offset-=10*m/w}else n===t.waveForm?_=0&&b.x=t.props.loopStart?t.updateLoopPos(t.props.loopStart,t.props.loopEnd+k):t.updateLoopPos(t.props.loopStart,t.props.loopStart))}else t._updateCursor(t._hitTest(u(l)),l)})}},{key:"updateSelection",value:function(t,e){t=Math.max(0,t),e=Math.min(this.duration,e),this.props.selectStart=t,this.props.selectEnd=e}},{key:"updateLoopPos",value:function(t,e){if(t<0){var r=Math.abs(t);e=Math.min(this.duration,e+r),t+=r}if(e>this.duration){var o=this.duration-e;t=Math.max(0,t+o),e+=o}t>e&&(t=e),t>=0&&e<=this.duration&&(this.props.loopStart=t,this.props.loopEnd=e)}},{key:"updateStartPos",value:function(t){var e=t/this.canvas.width*this.duration/this.props.hZoom+this.props.offset;this.props.startPosition=e}},{key:"offset",get:function(){return this.props.offset},set:function(t){this.props.offset=Math.max(0,Math.min(this.duration-this.displayDuration,t))}},{key:"buffer",get:function(){return this.props.buffer},set:function(t){this.props.buffer=t}},{key:"pixelRatio",get:function(){var t=this.canvas.getBoundingClientRect();return{x:this.props.width/t.width,y:this.props.height/t.height}}}]),e}(p.default);e.default=g,t.exports=e.default},function(t,e,r){"use strict";function o(t){if(Array.isArray(t)){for(var e=0,r=Array(t.length);e2&&void 0!==arguments[2]?arguments[2]:this;if(Array.isArray(t))return t.forEach(function(t){return r.$observe(t,e,o)}),this;if(void 0===this.$privateProps[t])throw new Error("Can' observe undefined prop "+t);return e.call(o,this.$privateProps[t]),this.$on("change:"+t,e,o),this}},{key:"$unobserve",value:function(t,e,r){var o=this;return Array.isArray(t)?(t.forEach(function(t){return o.$unobserve(t,e,r)}),this):(this.$off("change:"+t,e,r),this)}},{key:"$bind",value:function(t){for(var e=this,r=arguments.length,o=Array(r>1?r-1:0),n=1;n1?r-1:0),n=1;n1&&void 0!==arguments[1]?arguments[1]:null;return e=e||Object.keys(this.$privateProps).filter(function(e){return void 0!==t.$privateProps[e]}),t.$bind.apply(t,[this].concat(o(e))),this}},{key:"$unlink",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;return e=e||Object.keys(this.$privateProps).filter(function(e){return void 0!==t.$privateProps[e]}),t.$unbind.apply(t,[this].concat(o(Object.keys(this.$privateProps)))),this}}]),t}();Object.assign(u.prototype,a.default),e.default=u,t.exports=e.default},function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default={$on:function(t,e,r){return this._events=this._events||{},(this._events[t]||(this._events[t]=[])).push({callback:e,ctxArg:r,context:r||this}),this},$off:function(t,e,r){var o=this,n=void 0,i=void 0,s=void 0,a=void 0;if(!this._events||!this._events[t])return this;t||e||r||(this._events={});var u=this._events[t];if(u){if(a=[],e&&r)for(n=0,i=u.length;n=0;e--)o=n[e],o.callback.apply(o.context,r);return this},$triggerDeferred:function(t){var e=this;this._changeBatchTimers=this._changeBatchTimers||{},null==this._changeBatchTimers[t]&&(this._changeBatchTimers[t]=setTimeout(function(){e.$trigger(t),e._changeBatchTimers[t]=null},50))}},t.exports=e.default},function(t,e,r){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function s(t,e,r){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1,n=arguments.length>4&&void 0!==arguments[4]&&arguments[4];arguments[5];o=Math.max(1,o),r=Math.min(t.length,r),r=Math.min(e+100,r);for(var i=0,s=e;sl;n.beginPath(),n.moveTo(0,f),n.lineTo(r,f),n.stroke(),n.moveTo(0,f);var d=function(e,r,a,d){e=Math.max(0,Math.min(e,u)),r=Math.max(0,Math.min(r,u));var v=0,y=[];n.beginPath();var b=e*l;if(d){var m=(r-e)*l;n.fillStyle=d,n.fillRect(b,0,m,o)}for(var g=e;g=0?s(i,v,_,1,!0):0;if(k>=0){var w=k*t.props.vZoom*f;g===e&&n.moveTo(b,f-w),n.lineTo(b,f-w),y.push(b,w)}}else{var x=_>=0?s(i,v,_,1,!1):0,M=x*t.props.vZoom*f;g===e&&n.moveTo(0,f-M),n.lineTo(b,f-M)}b+=l,v=_}if(c){for(var P=y.length-1;P>0;P-=2){var O=y[P-1],E=y[P];n.lineTo(O,f+E)}n.fillStyle=a,n.fill()}else n.strokeStyle=a,n.stroke()};if(this.props.selectStart!==this.props.selectEnd){var v=this.duration/this.props.hZoom/u,y=this.props.selectStart-this.props.offset,b=Math.floor(y/v),m=this.props.selectEnd-this.props.offset,g=Math.floor(m/v);g>0&&b0&&d(0,b,this.props.color,this.props.background),d(Math.max(0,b),Math.min(u,g),this.props.selectColor,this.props.selectBackground),g60;)i/=5}else{i=[1,2,5][Math.round(r*n*2)]/n}return i}}]),e}(u.default);e.default=l,t.exports=e.default},function(t,e,r){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function n(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function i(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}Object.defineProperty(e,"__esModule",{value:!0});var s=function(){function t(t,e){for(var r=0;r\n* @Filename: CanvasUI.js\n*/\n\nimport Props from './Props';\n\nconst defaultProperties = {\n width: 640,\n height: 320,\n hZoom: 1,\n visible: true,\n duration: 'auto',\n color: '#fff',\n background: '#333',\n buffer: null\n};\n\nclass CanvasUI {\n\n constructor(defaults, props) {\n this.props = new Props(defaultProperties, defaults, props);\n this.props.$on('change', _ => { this.dirty = true; });\n this.dirty = true;\n this.canvas = document.createElement('canvas');\n this.ctx = this.canvas.getContext('2d');\n this.ctx.imageSmoothingEnabled = false;\n }\n\n renderIfDirty() {\n if (!this.dirty) return this;\n this.clear();\n if (!this.props.visible) return this;\n return this.render();\n }\n\n clear() {\n // clear canvas and update width / height\n this.canvas.width = this.props.width;\n this.canvas.height = this.props.height;\n }\n\n get duration() {\n return this.props.duration === 'auto' ? (this.props.buffer ? this.props.buffer.duration : 0) : this.props.duration;\n }\n\n get displayDuration() {\n return this.duration / this.props.hZoom;\n }\n}\n\nexport default CanvasUI;\n\n\n\n// WEBPACK FOOTER //\n// ./src/CanvasUI.js","/**\n * @Author: Rikard Lindstrom \n * @Filename: index.js\n */\n\n'use strict';\nimport SampleEditorView from './SampleEditorView';\n\n// expose on window\nwindow.SampleEditorView = SampleEditorView;\nexport default SampleEditorView;\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.js","/**\n * Main program, compiles everything onto one canvas and handles interaction\n * @Author: Rikard Lindstrom \n * @Filename: SampleEditorView.js\n */\n\nimport CanvasUI from './CanvasUI';\nimport Waveform from './Waveform';\nimport Ruler from './Ruler';\nimport LineMarker from './LineMarker';\nimport LoopMarker from './LoopMarker';\n\nconst defaultProperties = {\n hZoom: 1,\n vZoom: 2,\n offset: 0,\n background: '#ddd',\n color: '#222',\n selectColor: '#ddd',\n selectBackground: '#222',\n width: 640,\n height: 320,\n channel: 0,\n resolution: 1,\n startPosition: 42.1,\n uiZoomStickiness: 0.1,\n duration: 'auto',\n visible: true,\n loop: true,\n loopStart: 0,\n loopEnd: 1,\n selectStart: 0,\n selectEnd: 0,\n quantize: 0.0125,\n buffer: null\n};\n\nclass SampleEditorView extends CanvasUI {\n\n constructor(props) {\n\n super(defaultProperties, props);\n\n this.waveForm = new Waveform({});\n this.ruler = new Ruler({});\n this.startMarker = new LineMarker({});\n this.zoomMarker = new LineMarker({visible: false});\n this.loopLengthMarker = new LoopMarker({visible: true});\n this.loopStartMarker = new LineMarker({visible: true});\n this.loopEndMarker = new LineMarker({dir: -1, visible: true});\n\n this.waveForm.props.$link(this.props);\n this.ruler.props.$link(this.props, ['hZoom', 'width', 'offset', 'quantize',\n 'buffer' ]).$map(this.props, { height: v => v / 16 });\n this.zoomMarker.props.$link(this.props, ['height']);\n this.loopEndMarker.props.$link(this.props, ['height'])\n .$map(this.props, { width: v => v / 64 });\n this.loopStartMarker.props.$link(this.props, ['height'])\n .$map(this.props, { width: v => v / 64 });\n this.loopLengthMarker.props.$map(this.props, {\n height: v => v / 32\n });\n\n this.startMarker.props.$link(this.props, ['height'])\n .$map(this.props, { width: v => v / 64 });\n\n this.render = this.render.bind(this);\n\n this._setupUI();\n\n this.props.$on('defered_change', this.renderIfDirty, this);\n\n this.zoomMarker.props.$observe('visible', this.renderIfDirty, this);\n\n this.canvas.classList.add('SampleEditorView');\n }\n\n render() {\n let ctx = this.ctx;\n\n ctx.drawImage(this.waveForm.renderIfDirty().canvas, 0, 0);\n ctx.drawImage(this.zoomMarker.renderIfDirty().canvas,\n this.zoomMarker.position, 0);\n\n ctx.drawImage(this.ruler.renderIfDirty().canvas, 0, 0);\n\n ctx.drawImage(this.startMarker.renderIfDirty().canvas,\n this._timeToPixel(this.props.startPosition), 10);\n\n this.loopLengthMarker.props.width = this._timeToPixel(this.props.loopEnd) -\n this._timeToPixel(this.props.loopStart);\n\n if ( this.loopLengthMarker.props.width > 0 ){\n ctx.drawImage(this.loopLengthMarker.renderIfDirty().canvas,\n this._timeToPixel(this.props.loopStart), 20);\n }\n ctx.drawImage(this.loopStartMarker.renderIfDirty().canvas,\n this._timeToPixel(this.props.loopStart), 20);\n ctx.drawImage(this.loopEndMarker.renderIfDirty().canvas,\n this._timeToPixel(this.props.loopEnd) - this.loopEndMarker.props.width, 20);\n\n }\n\n _timeToPixel(time) {\n time -= this.props.offset;\n if (time === 0 || this.displayDuration === 0) {\n return 1;\n }\n let px = (time / this.displayDuration) * this.props.width;\n\n return Math.max(1, Math.round(px));\n }\n\n _pixelToTime(pixel) {\n return (pixel / this.props.width) * this.displayDuration;\n }\n\n _getLoopRect() {\n return {\n x1: this._timeToPixel(this.props.loopStart),\n y1: 20,\n x2: this._timeToPixel(this.props.loopEnd),\n y2: this.props.height - 20\n };\n }\n\n // a pretty crude hittest to find target from a relative mouse position\n _hitTest(point) {\n\n if (point.y < this.ruler.props.height) {\n\n if (this.props.loop) {\n let loopRect = this._getLoopRect();\n\n if (point.y >= loopRect.y1 && point.y < loopRect.y2) {\n if (Math.abs(point.x - loopRect.x1) < 5) {\n return this.loopStartMarker;\n } else if (Math.abs(point.x - loopRect.x2) < 5) {\n return this.loopEndMarker;\n } else if (point.x >= loopRect.x1 && point.x < loopRect.x2) {\n return this.loopLengthMarker;\n }\n }\n }\n\n if (point.x) {return this.ruler;}\n }\n return this.waveForm;\n }\n\n // update mouse cursor to reflect active target\n _updateCursor(hitTarget, e) {\n if (hitTarget === this.ruler) {\n this.canvas.style.cursor = 'pointer';\n } else if (hitTarget === this.loopStartMarker) {\n this.canvas.style.cursor = 'e-resize';\n } else if (hitTarget === this.loopEndMarker) {\n this.canvas.style.cursor = 'w-resize';\n } else if (hitTarget === this.loopLengthMarker) {\n this.canvas.style.cursor = 'move';\n } else if (hitTarget === this.waveForm && e.altKey) {\n this.canvas.style.cursor = 'zoom-in';\n } else {\n this.canvas.style.cursor = 'auto';\n }\n }\n\n // almost fully self contained ui interaction\n _setupUI() {\n let mouseDown = false;\n let lastY = 0;\n let zoomThresh = 0;\n let canvasTarget = null;\n let doZoom = false;\n let lastMousePos = { x: 0, y: 0 };\n let startXTime = 0;\n\n const toRelativeMovement = (e) => {\n let rect = this.canvas.getBoundingClientRect();\n let pixelRatio = this.pixelRatio;\n\n let x = (e.pageX - rect.left) * pixelRatio.x;\n let y = (e.pageY - rect.top) * pixelRatio.y;\n\n let movementX = e.movementX ? e.movementX * pixelRatio.x : 0;\n let movementY = e.movementY ? e.movementY * pixelRatio.y : 0;\n\n return {\n rect,\n x,\n y,\n movementX,\n movementY\n };\n };\n\n const quantizePosition = ({ x, y }) => {\n\n if (!this.props.quantize) return { x, y };\n\n let offsetPx = (this.props.offset / this.displayDuration) * this.props.width;\n let pxQuant = (this.props.quantize / this.displayDuration) * this.props.width;\n\n x += offsetPx;\n x = Math.round(x / pxQuant) * pxQuant;\n x -= offsetPx;\n return { x, y };\n };\n\n this.canvas.addEventListener('mousedown', (e)=>{\n mouseDown = true;\n lastY = null;\n zoomThresh = 0;\n doZoom = false;\n\n let pos = toRelativeMovement(e);\n\n canvasTarget = this._hitTest(pos);\n\n this._updateCursor(canvasTarget, e);\n\n if (canvasTarget === this.waveForm) {\n if (e.altKey) {\n doZoom = true;\n this.zoomMarker.position = pos.x;\n this.zoomMarker.props.visible = true;\n this.canvas.requestPointerLock();\n }\n }\n if (!doZoom) {\n pos = quantizePosition(pos);\n }\n startXTime = this._pixelToTime(pos.x) + this.props.offset;\n !doZoom && (this.props.selectEnd = this.props.selectStart = startXTime);\n lastMousePos = pos;\n\n });\n\n document.addEventListener('mouseup', ()=>{\n mouseDown = false;\n if (doZoom) {\n this.zoomMarker.props.visible = false;\n document.exitPointerLock();\n doZoom = false;\n }\n });\n\n document.addEventListener('mousemove', (e)=>{\n\n if (mouseDown) {\n\n let { x, y, movementX, movementY, rect } = toRelativeMovement(e);\n let p = { x, y };\n\n if (!doZoom) {\n\n // keep p within boarders of canvas\n p.x = Math.max(0, Math.min(this.props.width, p.x));\n p = quantizePosition(p);\n\n if (canvasTarget === this.ruler) {\n this.updateStartPos(p.x);\n }\n\n }\n\n let deltaX = (doZoom && movementX !== undefined ? movementX : (p.x - lastMousePos.x)) / rect.width;\n let deltaY = (doZoom && movementY !== undefined ? movementY : (p.y - lastMousePos.y)) / rect.height;\n\n let xTime = this._pixelToTime(p.x) + this.props.offset;\n let deltaTime = this._pixelToTime(deltaX) * this.props.width / this.pixelRatio.x;\n\n Object.assign(lastMousePos, p);\n\n if (doZoom) {\n\n if (lastY === null) lastY = p.y;\n\n lastY = p.y;\n\n zoomThresh += Math.abs(deltaY);\n let hZoom = Math.max(1, this.props.hZoom + deltaY * this.props.hZoom);\n\n if (zoomThresh > this.props.uiZoomStickiness) {\n\n let zoomDelta = hZoom - this.props.hZoom;\n\n if (zoomDelta !== 0 && hZoom >= 0.5) {\n\n let zoomPerc = zoomDelta / this.props.hZoom;\n let posRatio = p.x / (rect.width * this.pixelRatio.x);\n\n this.props.hZoom = hZoom;\n this.offset += zoomPerc * posRatio * this.displayDuration;\n\n }\n\n }\n\n this.offset -= (deltaX * 10) / hZoom;\n\n } else if (canvasTarget === this.waveForm) {\n\n if (xTime < startXTime) {\n this.updateSelection(this.props.selectStart + deltaTime, startXTime);\n } else {\n this.updateSelection(startXTime, this.props.selectEnd + deltaTime);\n }\n\n } else if (canvasTarget === this.ruler) {\n if (p.x >= 0 && p.x < rect.width) {\n this.updateStartPos(p.x);\n } else {\n if (p.x < 0) {\n this.offset = Math.max(0, this.props.offset - Math.abs(p.x * 0.1));\n } else {\n this.offset = Math.min(this.duration, this.props.offset + (p.x - rect.width) * 0.1);\n }\n }\n } else if (canvasTarget === this.loopLengthMarker) {\n\n this.updateLoopPos(this.props.loopStart + deltaTime, this.props.loopEnd + deltaTime);\n\n } else if (canvasTarget === this.loopStartMarker) {\n\n if (xTime <= this.props.loopEnd) {\n this.updateLoopPos(this.props.loopStart + deltaTime, this.props.loopEnd);\n } else {\n this.updateLoopPos(this.props.loopEnd, this.props.loopEnd);\n }\n\n } else if (canvasTarget === this.loopEndMarker) {\n\n if (xTime >= this.props.loopStart) {\n this.updateLoopPos(this.props.loopStart, this.props.loopEnd + deltaTime);\n } else {\n this.updateLoopPos(this.props.loopStart, this.props.loopStart);\n }\n\n }\n\n } else {\n this._updateCursor(this._hitTest(toRelativeMovement(e)), e);\n }\n });\n }\n\n updateSelection(start, end) {\n\n start = Math.max(0, start);\n end = Math.min(this.duration, end);\n\n this.props.selectStart = start;\n this.props.selectEnd = end;\n\n }\n\n updateLoopPos(start, end) {\n\n if (start < 0) {\n let d = Math.abs(start);\n\n end = Math.min(this.duration, end + d);\n start += d;\n\n }\n if (end > this.duration) {\n let d = this.duration - end;\n\n start = Math.max(0, start + d);\n end += d;\n }\n\n if (start > end) {\n start = end;\n }\n\n if (start >= 0 && end <= this.duration) {\n this.props.loopStart = start;\n this.props.loopEnd = end;\n }\n\n }\n\n updateStartPos(px) {\n let startPos = ((px / this.canvas.width) * this.duration / this.props.hZoom) + this.props.offset;\n\n this.props.startPosition = startPos;\n }\n\n get offset() {\n return this.props.offset;\n }\n\n set offset(v) {\n this.props.offset = Math.max(0, Math.min(this.duration - this.displayDuration, v));\n }\n\n get buffer() {\n return this.props.buffer;\n }\n\n set buffer(buffer) {\n this.props.buffer = buffer;\n }\n\n get pixelRatio() {\n\n let rect = this.canvas.getBoundingClientRect();\n let pixelRatio = { x: this.props.width / rect.width, y: this.props.height / rect.height };\n\n return pixelRatio;\n\n }\n}\n\nexport default SampleEditorView;\n\n\n\n// WEBPACK FOOTER //\n// ./src/SampleEditorView.js","/**\n * \"Magic\" Properties with bindings to react to changes\n * @Author: Rikard Lindstrom \n * @Filename: Props.js\n */\n\nimport eventMixin from './eventMixin';\n\nclass Props {\n\n /**\n * @constructor\n * @param {Object[]]} props - Properties that will me merged right to left.\n */\n constructor(...props) {\n\n props = props.filter(p => !!p);\n\n let propsObject = props.reduce((a, p) => { return Object.assign(a, p); }, {});\n\n Object.keys(propsObject).forEach(k => {\n Object.defineProperty(this, k, {\n get() {\n return propsObject[ k ];\n },\n set(v) {\n let oldV = propsObject[ k ];\n\n propsObject[ k ] = v;\n this.$triggerDeferred('defered_change');\n this.$trigger('change', k, v);\n this.$trigger(`change:${k}`, v, oldV);\n }\n });\n });\n\n this.$privateProps = propsObject;\n this.$keys = Object.keys(propsObject);\n }\n\n /**\n * Observe a value for changes, much like .$on but also initializes the callback\n * with current value\n * @param {string|string[]]} key - Key or keys to observe\n * @param {function} cb - Callback\n * @param {Object} ctx - Context for callback\n */\n $observe(key, cb, ctx = this) {\n if (Array.isArray(key)) {\n key.forEach(k => this.$observe(k, cb, ctx));\n return this;\n }\n if (this.$privateProps[ key ] === undefined) throw new Error('Can\\' observe undefined prop ' + key);\n cb.call(ctx, this.$privateProps[ key ]);\n this.$on(`change:${key}`, cb, ctx);\n return this;\n }\n\n /**\n * Unobserve a value for changes\n * @param {string|string[]]} key - Key or keys to observe\n * @param {function} cb - Callback\n * @param {Object} ctx - Context for callback\n */\n $unobserve(key, cb, ctx) {\n if (Array.isArray(key)) {\n key.forEach(k => this.$unobserve(k, cb, ctx));\n return this;\n }\n this.$off(`change:${key}`, cb, ctx);\n return this;\n }\n\n /**\n * Wrapper for $observe, sets the properties on the target instead of using a callback\n * @param {Object} target - Target context\n * @param {string|string[]]} key - Key or keys to observe\n */\n $bind(target, ...keys) {\n keys.forEach(k => {\n this.$observe(k, function (v) {\n target[ k ] = v;\n });\n });\n\n return this;\n }\n\n /**\n * Wrapper for $unobserve\n * @param {Object} target - Target context\n * @param {string|string[]]} key - Key or keys to observe\n */\n $unbind(target, ...keys) {\n if (keys && keys.length) {\n keys.forEach(k => { this.$unobserve(k, null, target); });\n } else {\n this.$off(null, null, target);\n }\n\n return this;\n }\n\n /**\n * Reversed $bind:ing between Props objects with filters.\n * @param {Props} otherProps - Another instance of Props\n * @param {Object} map - Mapping specification in the form of {propName: filterFn}\n */\n $map(otherProps, map) {\n Object.keys(map).forEach(k => {\n otherProps.$observe(k, v => {\n this[ k ] = map[ k ](v);\n });\n });\n\n return this;\n }\n\n /**\n * Reversed $bind:ing between Props objects.\n * @param {Props} otherProps - Another instance of Props\n * @param {string[]]} keys - Keys to observe\n */\n $link(otherProps, keys = null) {\n keys = keys || Object.keys(this.$privateProps).filter(k => otherProps.$privateProps[ k ] !== undefined);\n otherProps.$bind(this, ...keys);\n return this;\n }\n\n /**\n * Reversed $unbind:ing between Props objects with filters.\n * @param {Props} otherProps - Another instance of Props\n * @param {string[]]} keys - Keys to stop observing\n */\n $unlink(otherProps, keys = null) {\n keys = keys || Object.keys(this.$privateProps).filter(k => otherProps.$privateProps[ k ] !== undefined);\n otherProps.$unbind(this, ...Object.keys(this.$privateProps));\n return this;\n }\n}\n\nObject.assign(Props.prototype, eventMixin);\n\nexport default Props;\n\n\n\n// WEBPACK FOOTER //\n// ./src/Props.js","/**\n* Simple event $triggering and listening mixin, somewhat borrowed from Backbone\n* @author Rikard Lindstrom\n* @mixin\n*/\n'use strict';\n\nexport default {\n\n /**\n * Register a listener\n */\n $on: function (name, callback, context) {\n /** @member */\n this._events = this._events || {};\n\n let events = this._events[ name ] || (this._events[ name ] = []);\n\n events.push({\n callback: callback,\n ctxArg: context,\n context: context || this\n });\n\n return this;\n },\n\n /**\n * Unregister a listener\n */\n $off: function (name, callback, context) {\n let i, len, listener, retain;\n\n if (!this._events || !this._events[ name ]) {\n return this;\n }\n\n if (!name && !callback && !context) {\n this._events = {};\n }\n\n let eventListeners = this._events[ name ];\n\n if (eventListeners) {\n retain = [];\n // silly redundancy optimization, might be better to keep it DRY\n if (callback && context) {\n for (i = 0, len = eventListeners.length; i < len; i++) {\n listener = eventListeners[ i ];\n if (callback !== listener.callback && context !== listener.ctxArg) {\n retain.push(eventListeners[ i ]);\n }\n }\n } else if (callback) {\n for (i = 0, len = eventListeners.length; i < len; i++) {\n listener = eventListeners[ i ];\n if (callback !== listener.callback) {\n retain.push(eventListeners[ i ]);\n }\n }\n } else if (context) {\n for (i = 0, len = eventListeners.length; i < len; i++) {\n listener = eventListeners[ i ];\n if (context !== listener.ctxArg) {\n retain.push(eventListeners[ i ]);\n }\n }\n }\n\n this._events[ name ] = retain;\n } else if (context || callback) {\n Object.keys(this._events).forEach((k) => {\n this.$off(k, callback, context);\n });\n }\n\n if (!this._events[ name ].length) {\n delete this._events[ name ];\n }\n\n return this;\n },\n\n /**\n * $trigger an event\n */\n $trigger: function (name) {\n if (!this._events || !this._events[ name ]) {\n return this;\n }\n\n let i, args, binding, listeners;\n\n listeners = this._events[ name ];\n\n args = [].splice.call(arguments, 1);\n for (i = listeners.length - 1; i >= 0; i--) {\n binding = listeners[ i ];\n binding.callback.apply(binding.context, args);\n }\n\n return this;\n },\n\n /**\n * Triggered on the next heartbeat to avoid mass triggering\n * when changing multiple props at once\n */\n $triggerDeferred(event) {\n this._changeBatchTimers = this._changeBatchTimers || {};\n if (this._changeBatchTimers[ event ] == null) {\n this._changeBatchTimers[ event ] = setTimeout(()=>{\n this.$trigger(event);\n this._changeBatchTimers[ event ] = null;\n }, 50);\n }\n }\n\n};\n\n\n\n// WEBPACK FOOTER //\n// ./src/eventMixin.js","/**\n * @Author: Rikard Lindstrom \n * @Filename: Waveform.js\n */\n\nimport CanvasUI from './CanvasUI';\n\nconst defaultProperties = {\n vZoom: 1,\n offset: 0,\n background: '#ddd',\n color: '#222',\n selectColor: '#ddd',\n selectBackground: '#222',\n width: 640,\n height: 320,\n selectStart: 0,\n selectEnd: 0,\n channel: 0,\n resolution: 1\n};\n\nfunction avarage(data, from, to, interval = 1, abs = false, q) {\n\n interval = Math.max(1, interval);\n to = Math.min(data.length, to);\n to = Math.min(from + 100, to);\n\n let tot = 0;\n\n for (let i = from; i < to; i += interval) {\n tot += abs ? Math.abs(data[ i ]) : data[ i ];\n\n }\n let len = to - from;\n let avg = (tot / len) * interval;\n\n return avg;\n\n}\n\nclass Waveform extends CanvasUI {\n\n constructor(props) {\n\n super(defaultProperties, props);\n\n }\n\n render() {\n\n let buffer = this.props.buffer;\n\n if (!buffer) return this;\n\n let w = this.props.width;\n let h = this.props.height;\n\n let ctx = this.ctx;\n\n ctx.fillStyle = this.props.background;\n ctx.fillRect(0, 0, w, h);\n\n let data = buffer.getChannelData(this.props.channel);\n let displayLength = data.length / this.props.hZoom;\n let pointsToDraw = Math.min(w / this.props.resolution, displayLength);\n let bufferStepLen = displayLength / pointsToDraw ;\n let pointDistance = w / pointsToDraw ;\n\n let halfHeight = h / 2;\n\n let offsetSamples = this.props.offset * buffer.sampleRate;\n\n ctx.fillStyle = this.props.color;\n\n let drawSymetric = bufferStepLen > pointDistance;\n\n ctx.beginPath();\n ctx.moveTo(0, halfHeight);\n ctx.lineTo(w, halfHeight);\n ctx.stroke();\n\n ctx.moveTo(0, halfHeight);\n\n // internal render function to be able to divide the rendering on selection\n let draw = (from, to, color, background) => {\n\n from = Math.max(0, Math.min(from, pointsToDraw));\n to = Math.max(0, Math.min(to, pointsToDraw));\n\n let lastDataIndex = 0;\n let pointsDrawn = [];\n\n ctx.beginPath();\n\n let x = from * pointDistance;\n\n if (background) {\n let len = (to - from) * pointDistance;\n\n ctx.fillStyle = background;\n ctx.fillRect(x, 0, len, h);\n }\n\n for (let i = from; i < to; i++) {\n\n let j = Math.floor((i + 1) * bufferStepLen + offsetSamples);\n\n if (drawSymetric) {\n // avarage with abs\n let v = j >= 0 ? avarage(data, lastDataIndex, j, 1, true) : 0;\n\n if (v >= 0) {\n let y = v * this.props.vZoom * halfHeight;\n\n if (i === from) {\n ctx.moveTo(x, halfHeight - y);\n }\n ctx.lineTo(x, halfHeight - y);\n pointsDrawn.push(x, y);\n }\n\n } else {\n // avarage without abs\n let v = j >= 0 ? avarage(data, lastDataIndex, j, 1, false) : 0;\n let y = v * this.props.vZoom * halfHeight;\n\n if (i === from) {\n ctx.moveTo(0, halfHeight - y);\n }\n ctx.lineTo(x, halfHeight - y);\n\n }\n\n x += pointDistance;\n\n lastDataIndex = j;\n }\n\n // fill in the flip side if we should do a symetrical waveform\n if (drawSymetric) {\n for (let i = pointsDrawn.length - 1; i > 0; i -= 2) {\n let x = pointsDrawn[ i - 1 ];\n let y = pointsDrawn[ i ];\n\n ctx.lineTo(x, halfHeight + y);\n }\n\n ctx.fillStyle = color;\n ctx.fill();\n\n } else {\n ctx.strokeStyle = color;\n ctx.stroke();\n }\n\n };\n\n if (this.props.selectStart !== this.props.selectEnd) {\n let timePerPoint = (this.duration / this.props.hZoom) / pointsToDraw ;\n let relStartTime = this.props.selectStart - this.props.offset;\n let startPoint = Math.floor(relStartTime / timePerPoint);\n let relEndtTime = this.props.selectEnd - this.props.offset;\n let endPoint = Math.floor(relEndtTime / timePerPoint);\n\n if (endPoint > 0 && startPoint < pointsToDraw) {\n if (startPoint > 0) {\n draw(0, startPoint, this.props.color, this.props.background);\n }\n\n draw(Math.max(0, startPoint), Math.min(pointsToDraw, endPoint),\n this.props.selectColor, this.props.selectBackground);\n\n if (endPoint < pointsToDraw) {\n draw(endPoint, pointsToDraw, this.props.color, this.props.background);\n }\n } else {\n draw(0, pointsToDraw, this.props.color, this.props.background);\n }\n\n } else {\n draw(0, pointsToDraw, this.props.color, this.props.background);\n }\n\n return this;\n\n }\n\n}\n\nexport default Waveform;\n\n\n\n// WEBPACK FOOTER //\n// ./src/Waveform.js","/**\n * @Author: Rikard Lindstrom \n * @Filename: Ruler.js\n */\n\nimport CanvasUI from './CanvasUI';\n\nconst defaultProperties = {\n vZoom: 1,\n offset: 0,\n background: '#AAA',\n color: '#222',\n interval: 'auto',\n unit: 's',\n duration: 'auto',\n quantize: 0,\n height: 40\n};\n\nclass Ruler extends CanvasUI {\n\n constructor(props) {\n\n super(defaultProperties, props);\n\n }\n\n render() {\n\n let ctx = this.ctx;\n\n let w = this.props.width;\n let h = this.props.height;\n\n ctx.fillStyle = this.props.background;\n ctx.fillRect(0, 0, w, h);\n ctx.strokeStyle = ctx.fillStyle = this.props.color;\n ctx.lineWidth = 0.5;\n\n let displayDuration = this.displayDuration;\n let secondsPerPixel = displayDuration / w;\n\n let interval = this.props.interval;\n\n if (interval === 'auto') {\n interval = this.quantizeRuler(displayDuration);\n }\n\n interval = Math.max(0.001, interval);\n\n let precision = Math.max(2, Math.min(3, Math.round(1 / interval)));\n let pixelsPerInterval = (1 / secondsPerPixel) * interval;\n let drawPoints = w / pixelsPerInterval;\n\n let markerInterval = 5;// Math.max(1, Math.round(interval * 4));\n\n let x = -((this.props.offset / interval) % markerInterval) *\n pixelsPerInterval;\n\n let startTime = this.props.offset;\n\n for (let i = 0; i < drawPoints + markerInterval; i++) {\n let isMarker = i % markerInterval === 0;\n\n ctx.beginPath();\n if (isMarker) {\n ctx.moveTo(x, 0);\n ctx.lineTo(x, h);\n } else {\n ctx.moveTo(x, h);\n ctx.lineTo(x, h * 0.63);\n }\n ctx.stroke();\n\n if (isMarker) {\n let fontSize = this.props.width / 71;\n\n ctx.font = `${fontSize}px Arial`;\n ctx.fillText((startTime + (x / pixelsPerInterval) *\n interval).toFixed(precision) + this.props.unit, x + 5, fontSize);\n }\n\n x += pixelsPerInterval;\n\n }\n\n return this;\n\n }\n\n quantizeRuler(d) {\n const MAX_PIXEL_W = 20;\n const MIN_PIXEL_W = 60;\n\n let pixelsPerSecond = this.props.width / d;\n let r = 5 / pixelsPerSecond;\n let oct = -Math.floor(Math.log(r) / Math.log(10) + 1);\n let dec = (Math.pow(10, oct));\n\n let q;\n\n if (!this.props.quantize) {\n let c = [1, 2, 5][Math.round(r * dec * 2)];\n\n q = c / dec;\n\n } else {\n q = this.props.quantize;\n while (q * pixelsPerSecond < MAX_PIXEL_W) q += this.props.quantize;\n while (q * pixelsPerSecond > MIN_PIXEL_W) q /= 5;\n\n }\n return q;\n }\n\n}\n\nexport default Ruler;\n\n\n\n// WEBPACK FOOTER //\n// ./src/Ruler.js","/**\n * @Author: Rikard Lindstrom \n * @Filename: LineMarker.js\n */\n\nimport CanvasUI from './CanvasUI';\n\nconst defaultProperties = {\n background: '#999',\n color: '#222',\n width: 10,\n dir: 1\n};\n\nclass LineMarker extends CanvasUI {\n\n constructor(props) {\n\n super(defaultProperties, props);\n\n }\n\n render() {\n\n let ctx = this.ctx;\n\n // full clear and width / height set\n let w = this.props.width;\n let h = this.props.height;\n\n ctx.fillStyle = this.props.background;\n ctx.strokeStyle = this.props.color;\n\n ctx.beginPath();\n\n let triH = h / 64;\n let triH2 = triH * 2;\n\n if (this.props.dir === 1) {\n ctx.lineTo(w, triH);\n ctx.lineTo(0, triH2);\n ctx.lineTo(0, 0);\n ctx.lineTo(w, triH);\n ctx.fill();\n ctx.stroke();\n ctx.strokeRect(0, 0, 1, h);\n } else {\n ctx.moveTo(w, 0);\n ctx.lineTo(0, triH);\n ctx.lineTo(w, triH2);\n ctx.lineTo(w, 0);\n ctx.lineTo(0, triH);\n ctx.fill();\n ctx.stroke();\n ctx.strokeRect(w, 0, 1, h);\n }\n\n return this;\n }\n\n}\n\nexport default LineMarker;\n\n\n\n// WEBPACK FOOTER //\n// ./src/LineMarker.js","/**\n * @Author: Rikard Lindstrom \n * @Filename: LoopMarker.js\n */\n\nimport CanvasUI from './CanvasUI';\n\nconst defaultProperties = {\n height: 10,\n color: '#222'\n};\n\nclass LoopMarker extends CanvasUI {\n\n constructor(props) {\n\n super(defaultProperties, props);\n\n }\n\n render() {\n let ctx = this.ctx;\n\n // full clear and width / height set\n let w = this.props.width;\n let h = this.props.height;\n\n ctx.fillStyle = this.props.color;\n ctx.fillRect(0, 0, w, h);\n\n return this;\n }\n\n}\n\nexport default LoopMarker;\n\n\n\n// WEBPACK FOOTER //\n// ./src/LoopMarker.js"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/SampleEditorView.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define("SampleEditorView", [], factory); 6 | else if(typeof exports === 'object') 7 | exports["SampleEditorView"] = factory(); 8 | else 9 | root["SampleEditorView"] = factory(); 10 | })(typeof self !== 'undefined' ? self : this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { 50 | /******/ configurable: false, 51 | /******/ enumerable: true, 52 | /******/ get: getter 53 | /******/ }); 54 | /******/ } 55 | /******/ }; 56 | /******/ 57 | /******/ // getDefaultExport function for compatibility with non-harmony modules 58 | /******/ __webpack_require__.n = function(module) { 59 | /******/ var getter = module && module.__esModule ? 60 | /******/ function getDefault() { return module['default']; } : 61 | /******/ function getModuleExports() { return module; }; 62 | /******/ __webpack_require__.d(getter, 'a', getter); 63 | /******/ return getter; 64 | /******/ }; 65 | /******/ 66 | /******/ // Object.prototype.hasOwnProperty.call 67 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 68 | /******/ 69 | /******/ // __webpack_public_path__ 70 | /******/ __webpack_require__.p = ""; 71 | /******/ 72 | /******/ // Load entry module and return exports 73 | /******/ return __webpack_require__(__webpack_require__.s = 1); 74 | /******/ }) 75 | /************************************************************************/ 76 | /******/ ([ 77 | /* 0 */ 78 | /***/ (function(module, exports, __webpack_require__) { 79 | 80 | "use strict"; 81 | 82 | 83 | Object.defineProperty(exports, "__esModule", { 84 | value: true 85 | }); 86 | 87 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** 88 | * Base class for all ui 89 | * @Author: Rikard Lindstrom 90 | * @Filename: CanvasUI.js 91 | */ 92 | 93 | var _Props = __webpack_require__(3); 94 | 95 | var _Props2 = _interopRequireDefault(_Props); 96 | 97 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 98 | 99 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 100 | 101 | var defaultProperties = { 102 | width: 640, 103 | height: 320, 104 | hZoom: 1, 105 | visible: true, 106 | duration: 'auto', 107 | color: '#fff', 108 | background: '#333', 109 | buffer: null 110 | }; 111 | 112 | var CanvasUI = function () { 113 | function CanvasUI(defaults, props) { 114 | var _this = this; 115 | 116 | _classCallCheck(this, CanvasUI); 117 | 118 | this.props = new _Props2.default(defaultProperties, defaults, props); 119 | this.props.$on('change', function (_) { 120 | _this.dirty = true; 121 | }); 122 | this.dirty = true; 123 | this.canvas = document.createElement('canvas'); 124 | this.ctx = this.canvas.getContext('2d'); 125 | this.ctx.imageSmoothingEnabled = false; 126 | } 127 | 128 | _createClass(CanvasUI, [{ 129 | key: 'renderIfDirty', 130 | value: function renderIfDirty() { 131 | if (!this.dirty) return this; 132 | this.clear(); 133 | if (!this.props.visible) return this; 134 | return this.render(); 135 | } 136 | }, { 137 | key: 'clear', 138 | value: function clear() { 139 | // clear canvas and update width / height 140 | this.canvas.width = this.props.width; 141 | this.canvas.height = this.props.height; 142 | } 143 | }, { 144 | key: 'duration', 145 | get: function get() { 146 | return this.props.duration === 'auto' ? this.props.buffer ? this.props.buffer.duration : 0 : this.props.duration; 147 | } 148 | }, { 149 | key: 'displayDuration', 150 | get: function get() { 151 | return this.duration / this.props.hZoom; 152 | } 153 | }]); 154 | 155 | return CanvasUI; 156 | }(); 157 | 158 | exports.default = CanvasUI; 159 | module.exports = exports['default']; 160 | 161 | /***/ }), 162 | /* 1 */ 163 | /***/ (function(module, exports, __webpack_require__) { 164 | 165 | "use strict"; 166 | /** 167 | * @Author: Rikard Lindstrom 168 | * @Filename: index.js 169 | */ 170 | 171 | 172 | 173 | Object.defineProperty(exports, "__esModule", { 174 | value: true 175 | }); 176 | 177 | var _SampleEditorView = __webpack_require__(2); 178 | 179 | var _SampleEditorView2 = _interopRequireDefault(_SampleEditorView); 180 | 181 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 182 | 183 | // expose on window 184 | window.SampleEditorView = _SampleEditorView2.default; 185 | exports.default = _SampleEditorView2.default; 186 | module.exports = exports['default']; 187 | 188 | /***/ }), 189 | /* 2 */ 190 | /***/ (function(module, exports, __webpack_require__) { 191 | 192 | "use strict"; 193 | 194 | 195 | Object.defineProperty(exports, "__esModule", { 196 | value: true 197 | }); 198 | 199 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 200 | 201 | var _CanvasUI2 = __webpack_require__(0); 202 | 203 | var _CanvasUI3 = _interopRequireDefault(_CanvasUI2); 204 | 205 | var _Waveform = __webpack_require__(5); 206 | 207 | var _Waveform2 = _interopRequireDefault(_Waveform); 208 | 209 | var _Ruler = __webpack_require__(6); 210 | 211 | var _Ruler2 = _interopRequireDefault(_Ruler); 212 | 213 | var _LineMarker = __webpack_require__(7); 214 | 215 | var _LineMarker2 = _interopRequireDefault(_LineMarker); 216 | 217 | var _LoopMarker = __webpack_require__(8); 218 | 219 | var _LoopMarker2 = _interopRequireDefault(_LoopMarker); 220 | 221 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 222 | 223 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 224 | 225 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 226 | 227 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 228 | * Main program, compiles everything onto one canvas and handles interaction 229 | * @Author: Rikard Lindstrom 230 | * @Filename: SampleEditorView.js 231 | */ 232 | 233 | var defaultProperties = { 234 | hZoom: 1, 235 | vZoom: 2, 236 | offset: 0, 237 | background: '#ddd', 238 | color: '#222', 239 | selectColor: '#ddd', 240 | selectBackground: '#222', 241 | width: 640, 242 | height: 320, 243 | channel: 0, 244 | resolution: 1, 245 | startPosition: 42.1, 246 | uiZoomStickiness: 0.1, 247 | duration: 'auto', 248 | visible: true, 249 | loop: true, 250 | loopStart: 0, 251 | loopEnd: 1, 252 | selectStart: 0, 253 | selectEnd: 0, 254 | quantize: 0.0125, 255 | buffer: null 256 | }; 257 | 258 | var SampleEditorView = function (_CanvasUI) { 259 | _inherits(SampleEditorView, _CanvasUI); 260 | 261 | function SampleEditorView(props) { 262 | _classCallCheck(this, SampleEditorView); 263 | 264 | var _this = _possibleConstructorReturn(this, (SampleEditorView.__proto__ || Object.getPrototypeOf(SampleEditorView)).call(this, defaultProperties, props)); 265 | 266 | _this.waveForm = new _Waveform2.default({}); 267 | _this.ruler = new _Ruler2.default({}); 268 | _this.startMarker = new _LineMarker2.default({}); 269 | _this.zoomMarker = new _LineMarker2.default({ visible: false }); 270 | _this.loopLengthMarker = new _LoopMarker2.default({ visible: true }); 271 | _this.loopStartMarker = new _LineMarker2.default({ visible: true }); 272 | _this.loopEndMarker = new _LineMarker2.default({ dir: -1, visible: true }); 273 | 274 | _this.waveForm.props.$link(_this.props); 275 | _this.ruler.props.$link(_this.props, ['hZoom', 'width', 'offset', 'quantize', 'buffer']).$map(_this.props, { height: function height(v) { 276 | return v / 16; 277 | } }); 278 | _this.zoomMarker.props.$link(_this.props, ['height']); 279 | _this.loopEndMarker.props.$link(_this.props, ['height']).$map(_this.props, { width: function width(v) { 280 | return v / 64; 281 | } }); 282 | _this.loopStartMarker.props.$link(_this.props, ['height']).$map(_this.props, { width: function width(v) { 283 | return v / 64; 284 | } }); 285 | _this.loopLengthMarker.props.$map(_this.props, { 286 | height: function height(v) { 287 | return v / 32; 288 | } 289 | }); 290 | 291 | _this.startMarker.props.$link(_this.props, ['height']).$map(_this.props, { width: function width(v) { 292 | return v / 64; 293 | } }); 294 | 295 | _this.render = _this.render.bind(_this); 296 | 297 | _this._setupUI(); 298 | 299 | _this.props.$on('defered_change', _this.renderIfDirty, _this); 300 | 301 | _this.zoomMarker.props.$observe('visible', _this.renderIfDirty, _this); 302 | 303 | _this.canvas.classList.add('SampleEditorView'); 304 | return _this; 305 | } 306 | 307 | _createClass(SampleEditorView, [{ 308 | key: 'render', 309 | value: function render() { 310 | var ctx = this.ctx; 311 | 312 | ctx.drawImage(this.waveForm.renderIfDirty().canvas, 0, 0); 313 | ctx.drawImage(this.zoomMarker.renderIfDirty().canvas, this.zoomMarker.position, 0); 314 | 315 | ctx.drawImage(this.ruler.renderIfDirty().canvas, 0, 0); 316 | 317 | ctx.drawImage(this.startMarker.renderIfDirty().canvas, this._timeToPixel(this.props.startPosition), 10); 318 | 319 | this.loopLengthMarker.props.width = this._timeToPixel(this.props.loopEnd) - this._timeToPixel(this.props.loopStart); 320 | 321 | if (this.loopLengthMarker.props.width > 0) { 322 | ctx.drawImage(this.loopLengthMarker.renderIfDirty().canvas, this._timeToPixel(this.props.loopStart), 20); 323 | } 324 | ctx.drawImage(this.loopStartMarker.renderIfDirty().canvas, this._timeToPixel(this.props.loopStart), 20); 325 | ctx.drawImage(this.loopEndMarker.renderIfDirty().canvas, this._timeToPixel(this.props.loopEnd) - this.loopEndMarker.props.width, 20); 326 | } 327 | }, { 328 | key: '_timeToPixel', 329 | value: function _timeToPixel(time) { 330 | time -= this.props.offset; 331 | if (time === 0 || this.displayDuration === 0) { 332 | return 1; 333 | } 334 | var px = time / this.displayDuration * this.props.width; 335 | 336 | return Math.max(1, Math.round(px)); 337 | } 338 | }, { 339 | key: '_pixelToTime', 340 | value: function _pixelToTime(pixel) { 341 | return pixel / this.props.width * this.displayDuration; 342 | } 343 | }, { 344 | key: '_getLoopRect', 345 | value: function _getLoopRect() { 346 | return { 347 | x1: this._timeToPixel(this.props.loopStart), 348 | y1: 20, 349 | x2: this._timeToPixel(this.props.loopEnd), 350 | y2: this.props.height - 20 351 | }; 352 | } 353 | 354 | // a pretty crude hittest to find target from a relative mouse position 355 | 356 | }, { 357 | key: '_hitTest', 358 | value: function _hitTest(point) { 359 | 360 | if (point.y < this.ruler.props.height) { 361 | 362 | if (this.props.loop) { 363 | var loopRect = this._getLoopRect(); 364 | 365 | if (point.y >= loopRect.y1 && point.y < loopRect.y2) { 366 | if (Math.abs(point.x - loopRect.x1) < 5) { 367 | return this.loopStartMarker; 368 | } else if (Math.abs(point.x - loopRect.x2) < 5) { 369 | return this.loopEndMarker; 370 | } else if (point.x >= loopRect.x1 && point.x < loopRect.x2) { 371 | return this.loopLengthMarker; 372 | } 373 | } 374 | } 375 | 376 | if (point.x) { 377 | return this.ruler; 378 | } 379 | } 380 | return this.waveForm; 381 | } 382 | 383 | // update mouse cursor to reflect active target 384 | 385 | }, { 386 | key: '_updateCursor', 387 | value: function _updateCursor(hitTarget, e) { 388 | if (hitTarget === this.ruler) { 389 | this.canvas.style.cursor = 'pointer'; 390 | } else if (hitTarget === this.loopStartMarker) { 391 | this.canvas.style.cursor = 'e-resize'; 392 | } else if (hitTarget === this.loopEndMarker) { 393 | this.canvas.style.cursor = 'w-resize'; 394 | } else if (hitTarget === this.loopLengthMarker) { 395 | this.canvas.style.cursor = 'move'; 396 | } else if (hitTarget === this.waveForm && e.altKey) { 397 | this.canvas.style.cursor = 'zoom-in'; 398 | } else { 399 | this.canvas.style.cursor = 'auto'; 400 | } 401 | } 402 | 403 | // almost fully self contained ui interaction 404 | 405 | }, { 406 | key: '_setupUI', 407 | value: function _setupUI() { 408 | var _this2 = this; 409 | 410 | var mouseDown = false; 411 | var lastY = 0; 412 | var zoomThresh = 0; 413 | var canvasTarget = null; 414 | var doZoom = false; 415 | var lastMousePos = { x: 0, y: 0 }; 416 | var startXTime = 0; 417 | 418 | var toRelativeMovement = function toRelativeMovement(e) { 419 | var rect = _this2.canvas.getBoundingClientRect(); 420 | var pixelRatio = _this2.pixelRatio; 421 | 422 | var x = (e.pageX - rect.left) * pixelRatio.x; 423 | var y = (e.pageY - rect.top) * pixelRatio.y; 424 | 425 | var movementX = e.movementX ? e.movementX * pixelRatio.x : 0; 426 | var movementY = e.movementY ? e.movementY * pixelRatio.y : 0; 427 | 428 | return { 429 | rect: rect, 430 | x: x, 431 | y: y, 432 | movementX: movementX, 433 | movementY: movementY 434 | }; 435 | }; 436 | 437 | var quantizePosition = function quantizePosition(_ref) { 438 | var x = _ref.x, 439 | y = _ref.y; 440 | 441 | 442 | if (!_this2.props.quantize) return { x: x, y: y }; 443 | 444 | var offsetPx = _this2.props.offset / _this2.displayDuration * _this2.props.width; 445 | var pxQuant = _this2.props.quantize / _this2.displayDuration * _this2.props.width; 446 | 447 | x += offsetPx; 448 | x = Math.round(x / pxQuant) * pxQuant; 449 | x -= offsetPx; 450 | return { x: x, y: y }; 451 | }; 452 | 453 | this.canvas.addEventListener('mousedown', function (e) { 454 | mouseDown = true; 455 | lastY = null; 456 | zoomThresh = 0; 457 | doZoom = false; 458 | 459 | var pos = toRelativeMovement(e); 460 | 461 | canvasTarget = _this2._hitTest(pos); 462 | 463 | _this2._updateCursor(canvasTarget, e); 464 | 465 | if (canvasTarget === _this2.waveForm) { 466 | if (e.altKey) { 467 | doZoom = true; 468 | _this2.zoomMarker.position = pos.x; 469 | _this2.zoomMarker.props.visible = true; 470 | _this2.canvas.requestPointerLock(); 471 | } 472 | } 473 | if (!doZoom) { 474 | pos = quantizePosition(pos); 475 | } 476 | startXTime = _this2._pixelToTime(pos.x) + _this2.props.offset; 477 | !doZoom && (_this2.props.selectEnd = _this2.props.selectStart = startXTime); 478 | lastMousePos = pos; 479 | }); 480 | 481 | document.addEventListener('mouseup', function () { 482 | mouseDown = false; 483 | if (doZoom) { 484 | _this2.zoomMarker.props.visible = false; 485 | document.exitPointerLock(); 486 | doZoom = false; 487 | } 488 | }); 489 | 490 | document.addEventListener('mousemove', function (e) { 491 | 492 | if (mouseDown) { 493 | var _toRelativeMovement = toRelativeMovement(e), 494 | x = _toRelativeMovement.x, 495 | y = _toRelativeMovement.y, 496 | movementX = _toRelativeMovement.movementX, 497 | movementY = _toRelativeMovement.movementY, 498 | rect = _toRelativeMovement.rect; 499 | 500 | var p = { x: x, y: y }; 501 | 502 | if (!doZoom) { 503 | 504 | // keep p within boarders of canvas 505 | p.x = Math.max(0, Math.min(_this2.props.width, p.x)); 506 | p = quantizePosition(p); 507 | 508 | if (canvasTarget === _this2.ruler) { 509 | _this2.updateStartPos(p.x); 510 | } 511 | } 512 | 513 | var deltaX = (doZoom && movementX !== undefined ? movementX : p.x - lastMousePos.x) / rect.width; 514 | var deltaY = (doZoom && movementY !== undefined ? movementY : p.y - lastMousePos.y) / rect.height; 515 | 516 | var xTime = _this2._pixelToTime(p.x) + _this2.props.offset; 517 | var deltaTime = _this2._pixelToTime(deltaX) * _this2.props.width / _this2.pixelRatio.x; 518 | 519 | Object.assign(lastMousePos, p); 520 | 521 | if (doZoom) { 522 | 523 | if (lastY === null) lastY = p.y; 524 | 525 | lastY = p.y; 526 | 527 | zoomThresh += Math.abs(deltaY); 528 | var hZoom = Math.max(1, _this2.props.hZoom + deltaY * _this2.props.hZoom); 529 | 530 | if (zoomThresh > _this2.props.uiZoomStickiness) { 531 | 532 | var zoomDelta = hZoom - _this2.props.hZoom; 533 | 534 | if (zoomDelta !== 0 && hZoom >= 0.5) { 535 | 536 | var zoomPerc = zoomDelta / _this2.props.hZoom; 537 | var posRatio = p.x / (rect.width * _this2.pixelRatio.x); 538 | 539 | _this2.props.hZoom = hZoom; 540 | _this2.offset += zoomPerc * posRatio * _this2.displayDuration; 541 | } 542 | } 543 | 544 | _this2.offset -= deltaX * 10 / hZoom; 545 | } else if (canvasTarget === _this2.waveForm) { 546 | 547 | if (xTime < startXTime) { 548 | _this2.updateSelection(_this2.props.selectStart + deltaTime, startXTime); 549 | } else { 550 | _this2.updateSelection(startXTime, _this2.props.selectEnd + deltaTime); 551 | } 552 | } else if (canvasTarget === _this2.ruler) { 553 | if (p.x >= 0 && p.x < rect.width) { 554 | _this2.updateStartPos(p.x); 555 | } else { 556 | if (p.x < 0) { 557 | _this2.offset = Math.max(0, _this2.props.offset - Math.abs(p.x * 0.1)); 558 | } else { 559 | _this2.offset = Math.min(_this2.duration, _this2.props.offset + (p.x - rect.width) * 0.1); 560 | } 561 | } 562 | } else if (canvasTarget === _this2.loopLengthMarker) { 563 | 564 | _this2.updateLoopPos(_this2.props.loopStart + deltaTime, _this2.props.loopEnd + deltaTime); 565 | } else if (canvasTarget === _this2.loopStartMarker) { 566 | 567 | if (xTime <= _this2.props.loopEnd) { 568 | _this2.updateLoopPos(_this2.props.loopStart + deltaTime, _this2.props.loopEnd); 569 | } else { 570 | _this2.updateLoopPos(_this2.props.loopEnd, _this2.props.loopEnd); 571 | } 572 | } else if (canvasTarget === _this2.loopEndMarker) { 573 | 574 | if (xTime >= _this2.props.loopStart) { 575 | _this2.updateLoopPos(_this2.props.loopStart, _this2.props.loopEnd + deltaTime); 576 | } else { 577 | _this2.updateLoopPos(_this2.props.loopStart, _this2.props.loopStart); 578 | } 579 | } 580 | } else { 581 | _this2._updateCursor(_this2._hitTest(toRelativeMovement(e)), e); 582 | } 583 | }); 584 | } 585 | }, { 586 | key: 'updateSelection', 587 | value: function updateSelection(start, end) { 588 | 589 | start = Math.max(0, start); 590 | end = Math.min(this.duration, end); 591 | 592 | this.props.selectStart = start; 593 | this.props.selectEnd = end; 594 | } 595 | }, { 596 | key: 'updateLoopPos', 597 | value: function updateLoopPos(start, end) { 598 | 599 | if (start < 0) { 600 | var d = Math.abs(start); 601 | 602 | end = Math.min(this.duration, end + d); 603 | start += d; 604 | } 605 | if (end > this.duration) { 606 | var _d = this.duration - end; 607 | 608 | start = Math.max(0, start + _d); 609 | end += _d; 610 | } 611 | 612 | if (start > end) { 613 | start = end; 614 | } 615 | 616 | if (start >= 0 && end <= this.duration) { 617 | this.props.loopStart = start; 618 | this.props.loopEnd = end; 619 | } 620 | } 621 | }, { 622 | key: 'updateStartPos', 623 | value: function updateStartPos(px) { 624 | var startPos = px / this.canvas.width * this.duration / this.props.hZoom + this.props.offset; 625 | 626 | this.props.startPosition = startPos; 627 | } 628 | }, { 629 | key: 'offset', 630 | get: function get() { 631 | return this.props.offset; 632 | }, 633 | set: function set(v) { 634 | this.props.offset = Math.max(0, Math.min(this.duration - this.displayDuration, v)); 635 | } 636 | }, { 637 | key: 'buffer', 638 | get: function get() { 639 | return this.props.buffer; 640 | }, 641 | set: function set(buffer) { 642 | this.props.buffer = buffer; 643 | } 644 | }, { 645 | key: 'pixelRatio', 646 | get: function get() { 647 | 648 | var rect = this.canvas.getBoundingClientRect(); 649 | var pixelRatio = { x: this.props.width / rect.width, y: this.props.height / rect.height }; 650 | 651 | return pixelRatio; 652 | } 653 | }]); 654 | 655 | return SampleEditorView; 656 | }(_CanvasUI3.default); 657 | 658 | exports.default = SampleEditorView; 659 | module.exports = exports['default']; 660 | 661 | /***/ }), 662 | /* 3 */ 663 | /***/ (function(module, exports, __webpack_require__) { 664 | 665 | "use strict"; 666 | 667 | 668 | Object.defineProperty(exports, "__esModule", { 669 | value: true 670 | }); 671 | 672 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** 673 | * "Magic" Properties with bindings to react to changes 674 | * @Author: Rikard Lindstrom 675 | * @Filename: Props.js 676 | */ 677 | 678 | var _eventMixin = __webpack_require__(4); 679 | 680 | var _eventMixin2 = _interopRequireDefault(_eventMixin); 681 | 682 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 683 | 684 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 685 | 686 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 687 | 688 | var Props = function () { 689 | 690 | /** 691 | * @constructor 692 | * @param {Object[]]} props - Properties that will me merged right to left. 693 | */ 694 | function Props() { 695 | var _this = this; 696 | 697 | for (var _len = arguments.length, props = Array(_len), _key = 0; _key < _len; _key++) { 698 | props[_key] = arguments[_key]; 699 | } 700 | 701 | _classCallCheck(this, Props); 702 | 703 | props = props.filter(function (p) { 704 | return !!p; 705 | }); 706 | 707 | var propsObject = props.reduce(function (a, p) { 708 | return Object.assign(a, p); 709 | }, {}); 710 | 711 | Object.keys(propsObject).forEach(function (k) { 712 | Object.defineProperty(_this, k, { 713 | get: function get() { 714 | return propsObject[k]; 715 | }, 716 | set: function set(v) { 717 | var oldV = propsObject[k]; 718 | 719 | propsObject[k] = v; 720 | this.$triggerDeferred('defered_change'); 721 | this.$trigger('change', k, v); 722 | this.$trigger('change:' + k, v, oldV); 723 | } 724 | }); 725 | }); 726 | 727 | this.$privateProps = propsObject; 728 | this.$keys = Object.keys(propsObject); 729 | } 730 | 731 | /** 732 | * Observe a value for changes, much like .$on but also initializes the callback 733 | * with current value 734 | * @param {string|string[]]} key - Key or keys to observe 735 | * @param {function} cb - Callback 736 | * @param {Object} ctx - Context for callback 737 | */ 738 | 739 | 740 | _createClass(Props, [{ 741 | key: '$observe', 742 | value: function $observe(key, cb) { 743 | var _this2 = this; 744 | 745 | var ctx = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this; 746 | 747 | if (Array.isArray(key)) { 748 | key.forEach(function (k) { 749 | return _this2.$observe(k, cb, ctx); 750 | }); 751 | return this; 752 | } 753 | if (this.$privateProps[key] === undefined) throw new Error('Can\' observe undefined prop ' + key); 754 | cb.call(ctx, this.$privateProps[key]); 755 | this.$on('change:' + key, cb, ctx); 756 | return this; 757 | } 758 | 759 | /** 760 | * Unobserve a value for changes 761 | * @param {string|string[]]} key - Key or keys to observe 762 | * @param {function} cb - Callback 763 | * @param {Object} ctx - Context for callback 764 | */ 765 | 766 | }, { 767 | key: '$unobserve', 768 | value: function $unobserve(key, cb, ctx) { 769 | var _this3 = this; 770 | 771 | if (Array.isArray(key)) { 772 | key.forEach(function (k) { 773 | return _this3.$unobserve(k, cb, ctx); 774 | }); 775 | return this; 776 | } 777 | this.$off('change:' + key, cb, ctx); 778 | return this; 779 | } 780 | 781 | /** 782 | * Wrapper for $observe, sets the properties on the target instead of using a callback 783 | * @param {Object} target - Target context 784 | * @param {string|string[]]} key - Key or keys to observe 785 | */ 786 | 787 | }, { 788 | key: '$bind', 789 | value: function $bind(target) { 790 | var _this4 = this; 791 | 792 | for (var _len2 = arguments.length, keys = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 793 | keys[_key2 - 1] = arguments[_key2]; 794 | } 795 | 796 | keys.forEach(function (k) { 797 | _this4.$observe(k, function (v) { 798 | target[k] = v; 799 | }); 800 | }); 801 | 802 | return this; 803 | } 804 | 805 | /** 806 | * Wrapper for $unobserve 807 | * @param {Object} target - Target context 808 | * @param {string|string[]]} key - Key or keys to observe 809 | */ 810 | 811 | }, { 812 | key: '$unbind', 813 | value: function $unbind(target) { 814 | var _this5 = this; 815 | 816 | for (var _len3 = arguments.length, keys = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 817 | keys[_key3 - 1] = arguments[_key3]; 818 | } 819 | 820 | if (keys && keys.length) { 821 | keys.forEach(function (k) { 822 | _this5.$unobserve(k, null, target); 823 | }); 824 | } else { 825 | this.$off(null, null, target); 826 | } 827 | 828 | return this; 829 | } 830 | 831 | /** 832 | * Reversed $bind:ing between Props objects with filters. 833 | * @param {Props} otherProps - Another instance of Props 834 | * @param {Object} map - Mapping specification in the form of {propName: filterFn} 835 | */ 836 | 837 | }, { 838 | key: '$map', 839 | value: function $map(otherProps, map) { 840 | var _this6 = this; 841 | 842 | Object.keys(map).forEach(function (k) { 843 | otherProps.$observe(k, function (v) { 844 | _this6[k] = map[k](v); 845 | }); 846 | }); 847 | 848 | return this; 849 | } 850 | 851 | /** 852 | * Reversed $bind:ing between Props objects. 853 | * @param {Props} otherProps - Another instance of Props 854 | * @param {string[]]} keys - Keys to observe 855 | */ 856 | 857 | }, { 858 | key: '$link', 859 | value: function $link(otherProps) { 860 | var keys = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 861 | 862 | keys = keys || Object.keys(this.$privateProps).filter(function (k) { 863 | return otherProps.$privateProps[k] !== undefined; 864 | }); 865 | otherProps.$bind.apply(otherProps, [this].concat(_toConsumableArray(keys))); 866 | return this; 867 | } 868 | 869 | /** 870 | * Reversed $unbind:ing between Props objects with filters. 871 | * @param {Props} otherProps - Another instance of Props 872 | * @param {string[]]} keys - Keys to stop observing 873 | */ 874 | 875 | }, { 876 | key: '$unlink', 877 | value: function $unlink(otherProps) { 878 | var keys = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 879 | 880 | keys = keys || Object.keys(this.$privateProps).filter(function (k) { 881 | return otherProps.$privateProps[k] !== undefined; 882 | }); 883 | otherProps.$unbind.apply(otherProps, [this].concat(_toConsumableArray(Object.keys(this.$privateProps)))); 884 | return this; 885 | } 886 | }]); 887 | 888 | return Props; 889 | }(); 890 | 891 | Object.assign(Props.prototype, _eventMixin2.default); 892 | 893 | exports.default = Props; 894 | module.exports = exports['default']; 895 | 896 | /***/ }), 897 | /* 4 */ 898 | /***/ (function(module, exports, __webpack_require__) { 899 | 900 | "use strict"; 901 | /** 902 | * Simple event $triggering and listening mixin, somewhat borrowed from Backbone 903 | * @author Rikard Lindstrom 904 | * @mixin 905 | */ 906 | 907 | 908 | Object.defineProperty(exports, "__esModule", { 909 | value: true 910 | }); 911 | exports.default = { 912 | 913 | /** 914 | * Register a listener 915 | */ 916 | $on: function $on(name, callback, context) { 917 | /** @member */ 918 | this._events = this._events || {}; 919 | 920 | var events = this._events[name] || (this._events[name] = []); 921 | 922 | events.push({ 923 | callback: callback, 924 | ctxArg: context, 925 | context: context || this 926 | }); 927 | 928 | return this; 929 | }, 930 | 931 | /** 932 | * Unregister a listener 933 | */ 934 | $off: function $off(name, callback, context) { 935 | var _this = this; 936 | 937 | var i = void 0, 938 | len = void 0, 939 | listener = void 0, 940 | retain = void 0; 941 | 942 | if (!this._events || !this._events[name]) { 943 | return this; 944 | } 945 | 946 | if (!name && !callback && !context) { 947 | this._events = {}; 948 | } 949 | 950 | var eventListeners = this._events[name]; 951 | 952 | if (eventListeners) { 953 | retain = []; 954 | // silly redundancy optimization, might be better to keep it DRY 955 | if (callback && context) { 956 | for (i = 0, len = eventListeners.length; i < len; i++) { 957 | listener = eventListeners[i]; 958 | if (callback !== listener.callback && context !== listener.ctxArg) { 959 | retain.push(eventListeners[i]); 960 | } 961 | } 962 | } else if (callback) { 963 | for (i = 0, len = eventListeners.length; i < len; i++) { 964 | listener = eventListeners[i]; 965 | if (callback !== listener.callback) { 966 | retain.push(eventListeners[i]); 967 | } 968 | } 969 | } else if (context) { 970 | for (i = 0, len = eventListeners.length; i < len; i++) { 971 | listener = eventListeners[i]; 972 | if (context !== listener.ctxArg) { 973 | retain.push(eventListeners[i]); 974 | } 975 | } 976 | } 977 | 978 | this._events[name] = retain; 979 | } else if (context || callback) { 980 | Object.keys(this._events).forEach(function (k) { 981 | _this.$off(k, callback, context); 982 | }); 983 | } 984 | 985 | if (!this._events[name].length) { 986 | delete this._events[name]; 987 | } 988 | 989 | return this; 990 | }, 991 | 992 | /** 993 | * $trigger an event 994 | */ 995 | $trigger: function $trigger(name) { 996 | if (!this._events || !this._events[name]) { 997 | return this; 998 | } 999 | 1000 | var i = void 0, 1001 | args = void 0, 1002 | binding = void 0, 1003 | listeners = void 0; 1004 | 1005 | listeners = this._events[name]; 1006 | 1007 | args = [].splice.call(arguments, 1); 1008 | for (i = listeners.length - 1; i >= 0; i--) { 1009 | binding = listeners[i]; 1010 | binding.callback.apply(binding.context, args); 1011 | } 1012 | 1013 | return this; 1014 | }, 1015 | 1016 | /** 1017 | * Triggered on the next heartbeat to avoid mass triggering 1018 | * when changing multiple props at once 1019 | */ 1020 | $triggerDeferred: function $triggerDeferred(event) { 1021 | var _this2 = this; 1022 | 1023 | this._changeBatchTimers = this._changeBatchTimers || {}; 1024 | if (this._changeBatchTimers[event] == null) { 1025 | this._changeBatchTimers[event] = setTimeout(function () { 1026 | _this2.$trigger(event); 1027 | _this2._changeBatchTimers[event] = null; 1028 | }, 50); 1029 | } 1030 | } 1031 | }; 1032 | module.exports = exports['default']; 1033 | 1034 | /***/ }), 1035 | /* 5 */ 1036 | /***/ (function(module, exports, __webpack_require__) { 1037 | 1038 | "use strict"; 1039 | 1040 | 1041 | Object.defineProperty(exports, "__esModule", { 1042 | value: true 1043 | }); 1044 | 1045 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 1046 | 1047 | var _CanvasUI2 = __webpack_require__(0); 1048 | 1049 | var _CanvasUI3 = _interopRequireDefault(_CanvasUI2); 1050 | 1051 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1052 | 1053 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1054 | 1055 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1056 | 1057 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 1058 | * @Author: Rikard Lindstrom 1059 | * @Filename: Waveform.js 1060 | */ 1061 | 1062 | var defaultProperties = { 1063 | vZoom: 1, 1064 | offset: 0, 1065 | background: '#ddd', 1066 | color: '#222', 1067 | selectColor: '#ddd', 1068 | selectBackground: '#222', 1069 | width: 640, 1070 | height: 320, 1071 | selectStart: 0, 1072 | selectEnd: 0, 1073 | channel: 0, 1074 | resolution: 1 1075 | }; 1076 | 1077 | function avarage(data, from, to) { 1078 | var interval = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1; 1079 | var abs = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; 1080 | var q = arguments[5]; 1081 | 1082 | 1083 | interval = Math.max(1, interval); 1084 | to = Math.min(data.length, to); 1085 | to = Math.min(from + 100, to); 1086 | 1087 | var tot = 0; 1088 | 1089 | for (var i = from; i < to; i += interval) { 1090 | tot += abs ? Math.abs(data[i]) : data[i]; 1091 | } 1092 | var len = to - from; 1093 | var avg = tot / len * interval; 1094 | 1095 | return avg; 1096 | } 1097 | 1098 | var Waveform = function (_CanvasUI) { 1099 | _inherits(Waveform, _CanvasUI); 1100 | 1101 | function Waveform(props) { 1102 | _classCallCheck(this, Waveform); 1103 | 1104 | return _possibleConstructorReturn(this, (Waveform.__proto__ || Object.getPrototypeOf(Waveform)).call(this, defaultProperties, props)); 1105 | } 1106 | 1107 | _createClass(Waveform, [{ 1108 | key: 'render', 1109 | value: function render() { 1110 | var _this2 = this; 1111 | 1112 | var buffer = this.props.buffer; 1113 | 1114 | if (!buffer) return this; 1115 | 1116 | var w = this.props.width; 1117 | var h = this.props.height; 1118 | 1119 | var ctx = this.ctx; 1120 | 1121 | ctx.fillStyle = this.props.background; 1122 | ctx.fillRect(0, 0, w, h); 1123 | 1124 | var data = buffer.getChannelData(this.props.channel); 1125 | var displayLength = data.length / this.props.hZoom; 1126 | var pointsToDraw = Math.min(w / this.props.resolution, displayLength); 1127 | var bufferStepLen = displayLength / pointsToDraw; 1128 | var pointDistance = w / pointsToDraw; 1129 | 1130 | var halfHeight = h / 2; 1131 | 1132 | var offsetSamples = this.props.offset * buffer.sampleRate; 1133 | 1134 | ctx.fillStyle = this.props.color; 1135 | 1136 | var drawSymetric = bufferStepLen > pointDistance; 1137 | 1138 | ctx.beginPath(); 1139 | ctx.moveTo(0, halfHeight); 1140 | ctx.lineTo(w, halfHeight); 1141 | ctx.stroke(); 1142 | 1143 | ctx.moveTo(0, halfHeight); 1144 | 1145 | // internal render function to be able to divide the rendering on selection 1146 | var draw = function draw(from, to, color, background) { 1147 | 1148 | from = Math.max(0, Math.min(from, pointsToDraw)); 1149 | to = Math.max(0, Math.min(to, pointsToDraw)); 1150 | 1151 | var lastDataIndex = 0; 1152 | var pointsDrawn = []; 1153 | 1154 | ctx.beginPath(); 1155 | 1156 | var x = from * pointDistance; 1157 | 1158 | if (background) { 1159 | var len = (to - from) * pointDistance; 1160 | 1161 | ctx.fillStyle = background; 1162 | ctx.fillRect(x, 0, len, h); 1163 | } 1164 | 1165 | for (var i = from; i < to; i++) { 1166 | 1167 | var j = Math.floor((i + 1) * bufferStepLen + offsetSamples); 1168 | 1169 | if (drawSymetric) { 1170 | // avarage with abs 1171 | var v = j >= 0 ? avarage(data, lastDataIndex, j, 1, true) : 0; 1172 | 1173 | if (v >= 0) { 1174 | var y = v * _this2.props.vZoom * halfHeight; 1175 | 1176 | if (i === from) { 1177 | ctx.moveTo(x, halfHeight - y); 1178 | } 1179 | ctx.lineTo(x, halfHeight - y); 1180 | pointsDrawn.push(x, y); 1181 | } 1182 | } else { 1183 | // avarage without abs 1184 | var _v = j >= 0 ? avarage(data, lastDataIndex, j, 1, false) : 0; 1185 | var _y = _v * _this2.props.vZoom * halfHeight; 1186 | 1187 | if (i === from) { 1188 | ctx.moveTo(0, halfHeight - _y); 1189 | } 1190 | ctx.lineTo(x, halfHeight - _y); 1191 | } 1192 | 1193 | x += pointDistance; 1194 | 1195 | lastDataIndex = j; 1196 | } 1197 | 1198 | // fill in the flip side if we should do a symetrical waveform 1199 | if (drawSymetric) { 1200 | for (var _i = pointsDrawn.length - 1; _i > 0; _i -= 2) { 1201 | var _x3 = pointsDrawn[_i - 1]; 1202 | var _y2 = pointsDrawn[_i]; 1203 | 1204 | ctx.lineTo(_x3, halfHeight + _y2); 1205 | } 1206 | 1207 | ctx.fillStyle = color; 1208 | ctx.fill(); 1209 | } else { 1210 | ctx.strokeStyle = color; 1211 | ctx.stroke(); 1212 | } 1213 | }; 1214 | 1215 | if (this.props.selectStart !== this.props.selectEnd) { 1216 | var timePerPoint = this.duration / this.props.hZoom / pointsToDraw; 1217 | var relStartTime = this.props.selectStart - this.props.offset; 1218 | var startPoint = Math.floor(relStartTime / timePerPoint); 1219 | var relEndtTime = this.props.selectEnd - this.props.offset; 1220 | var endPoint = Math.floor(relEndtTime / timePerPoint); 1221 | 1222 | if (endPoint > 0 && startPoint < pointsToDraw) { 1223 | if (startPoint > 0) { 1224 | draw(0, startPoint, this.props.color, this.props.background); 1225 | } 1226 | 1227 | draw(Math.max(0, startPoint), Math.min(pointsToDraw, endPoint), this.props.selectColor, this.props.selectBackground); 1228 | 1229 | if (endPoint < pointsToDraw) { 1230 | draw(endPoint, pointsToDraw, this.props.color, this.props.background); 1231 | } 1232 | } else { 1233 | draw(0, pointsToDraw, this.props.color, this.props.background); 1234 | } 1235 | } else { 1236 | draw(0, pointsToDraw, this.props.color, this.props.background); 1237 | } 1238 | 1239 | return this; 1240 | } 1241 | }]); 1242 | 1243 | return Waveform; 1244 | }(_CanvasUI3.default); 1245 | 1246 | exports.default = Waveform; 1247 | module.exports = exports['default']; 1248 | 1249 | /***/ }), 1250 | /* 6 */ 1251 | /***/ (function(module, exports, __webpack_require__) { 1252 | 1253 | "use strict"; 1254 | 1255 | 1256 | Object.defineProperty(exports, "__esModule", { 1257 | value: true 1258 | }); 1259 | 1260 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 1261 | 1262 | var _CanvasUI2 = __webpack_require__(0); 1263 | 1264 | var _CanvasUI3 = _interopRequireDefault(_CanvasUI2); 1265 | 1266 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1267 | 1268 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1269 | 1270 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1271 | 1272 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 1273 | * @Author: Rikard Lindstrom 1274 | * @Filename: Ruler.js 1275 | */ 1276 | 1277 | var defaultProperties = { 1278 | vZoom: 1, 1279 | offset: 0, 1280 | background: '#AAA', 1281 | color: '#222', 1282 | interval: 'auto', 1283 | unit: 's', 1284 | duration: 'auto', 1285 | quantize: 0, 1286 | height: 40 1287 | }; 1288 | 1289 | var Ruler = function (_CanvasUI) { 1290 | _inherits(Ruler, _CanvasUI); 1291 | 1292 | function Ruler(props) { 1293 | _classCallCheck(this, Ruler); 1294 | 1295 | return _possibleConstructorReturn(this, (Ruler.__proto__ || Object.getPrototypeOf(Ruler)).call(this, defaultProperties, props)); 1296 | } 1297 | 1298 | _createClass(Ruler, [{ 1299 | key: 'render', 1300 | value: function render() { 1301 | 1302 | var ctx = this.ctx; 1303 | 1304 | var w = this.props.width; 1305 | var h = this.props.height; 1306 | 1307 | ctx.fillStyle = this.props.background; 1308 | ctx.fillRect(0, 0, w, h); 1309 | ctx.strokeStyle = ctx.fillStyle = this.props.color; 1310 | ctx.lineWidth = 0.5; 1311 | 1312 | var displayDuration = this.displayDuration; 1313 | var secondsPerPixel = displayDuration / w; 1314 | 1315 | var interval = this.props.interval; 1316 | 1317 | if (interval === 'auto') { 1318 | interval = this.quantizeRuler(displayDuration); 1319 | } 1320 | 1321 | interval = Math.max(0.001, interval); 1322 | 1323 | var precision = Math.max(2, Math.min(3, Math.round(1 / interval))); 1324 | var pixelsPerInterval = 1 / secondsPerPixel * interval; 1325 | var drawPoints = w / pixelsPerInterval; 1326 | 1327 | var markerInterval = 5; // Math.max(1, Math.round(interval * 4)); 1328 | 1329 | var x = -(this.props.offset / interval % markerInterval) * pixelsPerInterval; 1330 | 1331 | var startTime = this.props.offset; 1332 | 1333 | for (var i = 0; i < drawPoints + markerInterval; i++) { 1334 | var isMarker = i % markerInterval === 0; 1335 | 1336 | ctx.beginPath(); 1337 | if (isMarker) { 1338 | ctx.moveTo(x, 0); 1339 | ctx.lineTo(x, h); 1340 | } else { 1341 | ctx.moveTo(x, h); 1342 | ctx.lineTo(x, h * 0.63); 1343 | } 1344 | ctx.stroke(); 1345 | 1346 | if (isMarker) { 1347 | var fontSize = this.props.width / 71; 1348 | 1349 | ctx.font = fontSize + 'px Arial'; 1350 | ctx.fillText((startTime + x / pixelsPerInterval * interval).toFixed(precision) + this.props.unit, x + 5, fontSize); 1351 | } 1352 | 1353 | x += pixelsPerInterval; 1354 | } 1355 | 1356 | return this; 1357 | } 1358 | }, { 1359 | key: 'quantizeRuler', 1360 | value: function quantizeRuler(d) { 1361 | var MAX_PIXEL_W = 20; 1362 | var MIN_PIXEL_W = 60; 1363 | 1364 | var pixelsPerSecond = this.props.width / d; 1365 | var r = 5 / pixelsPerSecond; 1366 | var oct = -Math.floor(Math.log(r) / Math.log(10) + 1); 1367 | var dec = Math.pow(10, oct); 1368 | 1369 | var q = void 0; 1370 | 1371 | if (!this.props.quantize) { 1372 | var c = [1, 2, 5][Math.round(r * dec * 2)]; 1373 | 1374 | q = c / dec; 1375 | } else { 1376 | q = this.props.quantize; 1377 | while (q * pixelsPerSecond < MAX_PIXEL_W) { 1378 | q += this.props.quantize; 1379 | }while (q * pixelsPerSecond > MIN_PIXEL_W) { 1380 | q /= 5; 1381 | } 1382 | } 1383 | return q; 1384 | } 1385 | }]); 1386 | 1387 | return Ruler; 1388 | }(_CanvasUI3.default); 1389 | 1390 | exports.default = Ruler; 1391 | module.exports = exports['default']; 1392 | 1393 | /***/ }), 1394 | /* 7 */ 1395 | /***/ (function(module, exports, __webpack_require__) { 1396 | 1397 | "use strict"; 1398 | 1399 | 1400 | Object.defineProperty(exports, "__esModule", { 1401 | value: true 1402 | }); 1403 | 1404 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 1405 | 1406 | var _CanvasUI2 = __webpack_require__(0); 1407 | 1408 | var _CanvasUI3 = _interopRequireDefault(_CanvasUI2); 1409 | 1410 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1411 | 1412 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1413 | 1414 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1415 | 1416 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 1417 | * @Author: Rikard Lindstrom 1418 | * @Filename: LineMarker.js 1419 | */ 1420 | 1421 | var defaultProperties = { 1422 | background: '#999', 1423 | color: '#222', 1424 | width: 10, 1425 | dir: 1 1426 | }; 1427 | 1428 | var LineMarker = function (_CanvasUI) { 1429 | _inherits(LineMarker, _CanvasUI); 1430 | 1431 | function LineMarker(props) { 1432 | _classCallCheck(this, LineMarker); 1433 | 1434 | return _possibleConstructorReturn(this, (LineMarker.__proto__ || Object.getPrototypeOf(LineMarker)).call(this, defaultProperties, props)); 1435 | } 1436 | 1437 | _createClass(LineMarker, [{ 1438 | key: 'render', 1439 | value: function render() { 1440 | 1441 | var ctx = this.ctx; 1442 | 1443 | // full clear and width / height set 1444 | var w = this.props.width; 1445 | var h = this.props.height; 1446 | 1447 | ctx.fillStyle = this.props.background; 1448 | ctx.strokeStyle = this.props.color; 1449 | 1450 | ctx.beginPath(); 1451 | 1452 | var triH = h / 64; 1453 | var triH2 = triH * 2; 1454 | 1455 | if (this.props.dir === 1) { 1456 | ctx.lineTo(w, triH); 1457 | ctx.lineTo(0, triH2); 1458 | ctx.lineTo(0, 0); 1459 | ctx.lineTo(w, triH); 1460 | ctx.fill(); 1461 | ctx.stroke(); 1462 | ctx.strokeRect(0, 0, 1, h); 1463 | } else { 1464 | ctx.moveTo(w, 0); 1465 | ctx.lineTo(0, triH); 1466 | ctx.lineTo(w, triH2); 1467 | ctx.lineTo(w, 0); 1468 | ctx.lineTo(0, triH); 1469 | ctx.fill(); 1470 | ctx.stroke(); 1471 | ctx.strokeRect(w, 0, 1, h); 1472 | } 1473 | 1474 | return this; 1475 | } 1476 | }]); 1477 | 1478 | return LineMarker; 1479 | }(_CanvasUI3.default); 1480 | 1481 | exports.default = LineMarker; 1482 | module.exports = exports['default']; 1483 | 1484 | /***/ }), 1485 | /* 8 */ 1486 | /***/ (function(module, exports, __webpack_require__) { 1487 | 1488 | "use strict"; 1489 | 1490 | 1491 | Object.defineProperty(exports, "__esModule", { 1492 | value: true 1493 | }); 1494 | 1495 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 1496 | 1497 | var _CanvasUI2 = __webpack_require__(0); 1498 | 1499 | var _CanvasUI3 = _interopRequireDefault(_CanvasUI2); 1500 | 1501 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 1502 | 1503 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 1504 | 1505 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 1506 | 1507 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 1508 | * @Author: Rikard Lindstrom 1509 | * @Filename: LoopMarker.js 1510 | */ 1511 | 1512 | var defaultProperties = { 1513 | height: 10, 1514 | color: '#222' 1515 | }; 1516 | 1517 | var LoopMarker = function (_CanvasUI) { 1518 | _inherits(LoopMarker, _CanvasUI); 1519 | 1520 | function LoopMarker(props) { 1521 | _classCallCheck(this, LoopMarker); 1522 | 1523 | return _possibleConstructorReturn(this, (LoopMarker.__proto__ || Object.getPrototypeOf(LoopMarker)).call(this, defaultProperties, props)); 1524 | } 1525 | 1526 | _createClass(LoopMarker, [{ 1527 | key: 'render', 1528 | value: function render() { 1529 | var ctx = this.ctx; 1530 | 1531 | // full clear and width / height set 1532 | var w = this.props.width; 1533 | var h = this.props.height; 1534 | 1535 | ctx.fillStyle = this.props.color; 1536 | ctx.fillRect(0, 0, w, h); 1537 | 1538 | return this; 1539 | } 1540 | }]); 1541 | 1542 | return LoopMarker; 1543 | }(_CanvasUI3.default); 1544 | 1545 | exports.default = LoopMarker; 1546 | module.exports = exports['default']; 1547 | 1548 | /***/ }) 1549 | /******/ ]); 1550 | }); 1551 | //# sourceMappingURL=SampleEditorView.js.map --------------------------------------------------------------------------------