Skip to content

Commit

Permalink
Merge pull request katspaugh#679 from chrisparton1991/feature/multi-c…
Browse files Browse the repository at this point in the history
…anvas-renderer

Multi-Canvas Renderer
  • Loading branch information
katspaugh committed Mar 26, 2016
2 parents d906fca + 05f615f commit 9cab0f4
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 0 deletions.
15 changes: 15 additions & 0 deletions spec/util.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,19 @@ describe('util', function() {
expect(WaveSurfer.util.getId()).toStartWith('wavesurfer_');
});

it('min returns the smallest number in the provided array', function() {
expect(WaveSurfer.util.min([0, 1, 1.1, 100, -1])).toEqual(-1);
});

it('min returns +Infinity for an empty array', function() {
expect(WaveSurfer.util.min([])).toEqual(+Infinity);
});

it('max returns the largest number in the provided array', function() {
expect(WaveSurfer.util.max([0, 1, 1.1, 100, -1])).toEqual(100);
});

it('max returns -Infinity for an empty array', function() {
expect(WaveSurfer.util.max([])).toEqual(-Infinity);
});
});
3 changes: 3 additions & 0 deletions src/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ WaveSurfer.Drawer = {

this.lastPos = 0;

this.initDrawer(params);
this.createWrapper();
this.createElements();
},
Expand Down Expand Up @@ -182,6 +183,8 @@ WaveSurfer.Drawer = {
},

/* Renderer-specific methods */
initDrawer: function () {},

createElements: function () {},

updateSize: function () {},
Expand Down
309 changes: 309 additions & 0 deletions src/drawer.multicanvas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
'use strict';

WaveSurfer.Drawer.MultiCanvas = Object.create(WaveSurfer.Drawer);

WaveSurfer.util.extend(WaveSurfer.Drawer.MultiCanvas, {

initDrawer: function (params) {
this.maxCanvasWidth = params.maxCanvasWidth != null ? params.maxCanvasWidth : 4000;
this.maxCanvasElementWidth = Math.round(this.maxCanvasWidth / this.params.pixelRatio);

if (this.maxCanvasWidth <= 1) {
throw 'maxCanvasWidth must be greater than 1.';
} else if (this.maxCanvasWidth % 2 == 1) {
throw 'maxCanvasWidth must be an even number.';
}

this.hasProgressCanvas = this.params.waveColor != this.params.progressColor;
this.halfPixel = 0.5 / this.params.pixelRatio;
this.canvases = [];
},

createElements: function () {
this.progressWave = this.wrapper.appendChild(
this.style(document.createElement('wave'), {
position: 'absolute',
zIndex: 2,
left: 0,
top: 0,
bottom: 0,
overflow: 'hidden',
width: '0',
display: 'none',
boxSizing: 'border-box',
borderRightStyle: 'solid',
borderRightWidth: this.params.cursorWidth + 'px',
borderRightColor: this.params.cursorColor
})
);

this.addCanvas();
},

updateSize: function () {
var totalWidth = Math.round(this.width / this.params.pixelRatio),
requiredCanvases = Math.ceil(totalWidth / this.maxCanvasElementWidth);

while (this.canvases.length < requiredCanvases) {
this.addCanvas();
}

while (this.canvases.length > requiredCanvases) {
this.removeCanvas();
}

for (var i in this.canvases) {
// Add some overlap to prevent vertical white stripes, keep the width even for simplicity.
var canvasWidth = this.maxCanvasWidth + 2 * Math.ceil(this.params.pixelRatio / 2);

if (i == this.canvases.length - 1) {
canvasWidth = this.width - (this.maxCanvasWidth * (this.canvases.length - 1));
}

this.updateDimensions(this.canvases[i], canvasWidth, this.height);
this.clearWave(this.canvases[i]);
}
},

addCanvas: function () {
var entry = {};
var leftOffset = this.maxCanvasElementWidth * this.canvases.length;

entry.wave = this.wrapper.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
zIndex: 1,
left: leftOffset + 'px',
top: 0,
bottom: 0
})
);
entry.waveCtx = entry.wave.getContext('2d');

if (this.hasProgressCanvas) {
entry.progress = this.progressWave.appendChild(
this.style(document.createElement('canvas'), {
position: 'absolute',
left: leftOffset + 'px',
top: 0,
bottom: 0
})
);
entry.progressCtx = entry.progress.getContext('2d');
}

this.canvases.push(entry);
},

removeCanvas: function () {
var lastEntry = this.canvases.pop();
lastEntry.wave.parentElement.removeChild(lastEntry.wave);
if (this.hasProgressCanvas) {
lastEntry.progress.parentElement.removeChild(lastEntry.progress);
}
},

updateDimensions: function (entry, width, height) {
var elementWidth = Math.round(width / this.params.pixelRatio);

entry.waveCtx.canvas.width = width;
entry.waveCtx.canvas.height = height;
this.style(entry.waveCtx.canvas, { width: elementWidth + 'px'});

this.style(this.progressWave, { display: 'block'});

if (this.hasProgressCanvas) {
entry.progressCtx.canvas.width = width;
entry.progressCtx.canvas.height = height;
this.style(entry.progressCtx.canvas, { width: elementWidth + 'px'});
}
},

clearWave: function (entry) {
entry.waveCtx.clearRect(0, 0, entry.waveCtx.canvas.width, entry.waveCtx.canvas.height);
if (this.hasProgressCanvas) {
entry.progressCtx.clearRect(0, 0, entry.progressCtx.canvas.width, entry.progressCtx.canvas.height);
}
},

drawBars: function (peaks, channelIndex) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(this.drawBars, this);
return;
} else {
peaks = channels[0];
}
}

// Bar wave draws the bottom only as a reflection of the top,
// so we don't need negative values
var hasMinVals = [].some.call(peaks, function (val) { return val < 0; });
if (hasMinVals) {
peaks = [].filter.call(peaks, function (_, index) { return index % 2 == 0; });
}

// A half-pixel offset makes lines crisp
var width = this.width;
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = peaks.length;
var bar = this.params.barWidth * this.params.pixelRatio;
var gap = Math.max(this.params.pixelRatio, ~~(bar / 2));
var step = bar + gap;

var absmax = 1;
if (this.params.normalize) {
absmax = WaveSurfer.util.max(peaks);
}

var scale = length / width;

this.canvases[0].waveCtx.fillStyle = this.params.waveColor;
if (this.canvases[0].progressCtx) {
this.canvases[0].progressCtx.fillStyle = this.params.progressColor;
}

for (var i = 0; i < width; i += step) {
var h = Math.round(peaks[Math.floor(i * scale)] / absmax * halfH);
this.fillRect(i + this.halfPixel, halfH - h + offsetY, bar + this.halfPixel, h * 2);
}
},

drawWave: function (peaks, channelIndex) {
// Split channels
if (peaks[0] instanceof Array) {
var channels = peaks;
if (this.params.splitChannels) {
this.setHeight(channels.length * this.params.height * this.params.pixelRatio);
channels.forEach(this.drawWave, this);
return;
} else {
peaks = channels[0];
}
}

// Support arrays without negative peaks
var hasMinValues = [].some.call(peaks, function (val) { return val < 0; });
if (!hasMinValues) {
var reflectedPeaks = [];
for (var i = 0, len = peaks.length; i < len; i++) {
reflectedPeaks[2 * i] = peaks[i];
reflectedPeaks[2 * i + 1] = -peaks[i];
}
peaks = reflectedPeaks;
}

// A half-pixel offset makes lines crisp
var height = this.params.height * this.params.pixelRatio;
var offsetY = height * channelIndex || 0;
var halfH = height / 2;
var length = ~~(peaks.length / 2);

var scale = 1;
if (this.params.fillParent && this.width != length) {
scale = this.width / length;
}

var absmax = 1;
if (this.params.normalize) {
var max = WaveSurfer.util.max(peaks);
var min = WaveSurfer.util.min(peaks);
absmax = -min > max ? -min : max;
}

this.drawLine(length, peaks, absmax, halfH, scale, offsetY);

// Always draw a median line
this.fillRect(0, halfH + offsetY - this.halfPixel, this.width, this.halfPixel);
},

drawLine: function (length, peaks, absmax, halfH, scale, offsetY) {
for (var index in this.canvases) {
var entry = this.canvases[index];

this.setFillStyles(entry);

this.drawLineToContext(entry.waveCtx, index, peaks, absmax, halfH, scale, offsetY);
this.drawLineToContext(entry.progressCtx, index, peaks, absmax, halfH, scale, offsetY);
}
},

drawLineToContext: function (ctx, index, peaks, absmax, halfH, scale, offsetY) {
if (!ctx) { return; }

var first = index * this.maxCanvasWidth,
last = first + ctx.canvas.width + 1;

ctx.beginPath();
ctx.moveTo(this.halfPixel, halfH + offsetY);

for (var i = first; i < last; i++) {
var h = Math.round(peaks[2 * i] / absmax * halfH);
ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
}

// Draw the bottom edge going backwards, to make a single
// closed hull to fill.
for (var i = last - 1; i >= first; i--) {
var h = Math.round(peaks[2 * i + 1] / absmax * halfH);
ctx.lineTo((i - first) * scale + this.halfPixel, halfH - h + offsetY);
}

ctx.closePath();
ctx.fill();
},

fillRect: function (x, y, width, height) {
for (var i in this.canvases) {
var entry = this.canvases[i],
leftOffset = i * this.maxCanvasWidth;

var intersection = {
x1: Math.max(x, i * this.maxCanvasWidth),
y1: y,
x2: Math.min(x + width, i * this.maxCanvasWidth + entry.waveCtx.canvas.width),
y2: y + height
};

if (intersection.x1 < intersection.x2) {
this.setFillStyles(entry);

this.fillRectToContext(entry.waveCtx,
intersection.x1 - leftOffset,
intersection.y1,
intersection.x2 - intersection.x1,
intersection.y2 - intersection.y1);

this.fillRectToContext(entry.progressCtx,
intersection.x1 - leftOffset,
intersection.y1,
intersection.x2 - intersection.x1,
intersection.y2 - intersection.y1);
}
}
},

fillRectToContext: function (ctx, x, y, width, height) {
if (!ctx) { return; }
ctx.fillRect(x, y, width, height);
},

setFillStyles: function (entry) {
entry.waveCtx.fillStyle = this.params.waveColor;
if (this.hasProgressCanvas) {
entry.progressCtx.fillStyle = this.params.progressColor;
}
},

updateProgress: function (progress) {
var pos = Math.round(
this.width * progress
) / this.params.pixelRatio;
this.style(this.progressWave, { width: pos + 'px' });
}
});
22 changes: 22 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ WaveSurfer.util = {
return dest;
},

min: function(values) {
var min = +Infinity;
for (var i in values) {
if (values[i] < min) {
min = values[i];
}
}

return min;
},

max: function(values) {
var max = -Infinity;
for (var i in values) {
if (values[i] > max) {
max = values[i];
}
}

return max;
},

getId: function () {
return 'wavesurfer_' + Math.random().toString(32).substring(2);
},
Expand Down

0 comments on commit 9cab0f4

Please sign in to comment.