Use morganherlocker/cubic-spline for spline

This allows us to do a simpler line interpolation which is closer to
what Winamp actually does:

1. Derive a Y value for every column
2. Draw a pixel at the Y value for the first column
3. For each successive column:
3a. Draw a vertical line from the previous Y value to the current Y value
This commit is contained in:
Jordan Eldredge 2018-11-04 15:17:10 -08:00
parent 848d649ac6
commit 491c7591d0
4 changed files with 110 additions and 169 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

View file

@ -2,8 +2,7 @@ import React from "react";
import { connect } from "react-redux";
import { percentToRange, clamp } from "../../utils";
import { BANDS } from "../../constants";
import { getCurvePoints } from "./spline";
import line from "./bresenham";
import spline from "./spline";
const GRAPH_HEIGHT = 19;
const GRAPH_WIDTH = 113;
@ -81,36 +80,24 @@ class EqGraph extends React.Component {
const min = 0;
const max = GRAPH_HEIGHT - 1;
const points = amplitudes.reduce((prev, value, i) => {
const xs = [];
const ys = [];
amplitudes.forEach((value, i) => {
const percent = (100 - value) / 100;
const y = percentToRange(percent, min, max);
const x = i * 12; // Each band is 12 pixels wide
return prev.concat(x, y);
}, []);
// Spline between points in order to create nice curves
const tension = 0.8;
const resolution = 4; // Points in each segment
const smoothPoints = getCurvePoints(points, tension, resolution);
const smoothPointCoords = [];
for (let i = 0; i < smoothPoints.length; i += 2) {
// Splining can push peaks out of bounds. So we fudge them back in.
const x = Math.round(smoothPoints[i]);
const y = Math.round(clamp(smoothPoints[i + 1], min, max));
smoothPointCoords.push({ x, y });
}
let prev = smoothPointCoords.shift();
smoothPointCoords.forEach(next => {
for (const point of line(prev, next)) {
// Note: Technially, we are double drawing each point given to us by
// getCurvePoints, since the end of each line is the same as the start
// of the next.
this.canvasCtx.fillRect(paddingLeft + point.x, point.y, 1, 1);
}
prev = next;
// Each band is 12 pixels widex
xs.push(i * 12);
ys.push(percentToRange(percent, min, max));
});
const maxX = xs[xs.length - 1];
let lastY = ys[0];
for (let x = 0; x <= maxX; x++) {
const y = clamp(Math.round(spline(x, xs, ys)), 0, GRAPH_HEIGHT - 1);
const yTop = Math.min(y, lastY);
const height = 1 + Math.abs(lastY - y);
this.canvasCtx.fillRect(paddingLeft + x, yTop, 1, height);
lastY = y;
}
}
drawPreampLine() {

View file

@ -1,71 +0,0 @@
// Adapted from https://github.com/nquicenob/bresenham-line by Nicolas Quiceno
interface Point {
x: number;
y: number;
}
type Sign = 1 | -1;
function getSing(num: number): Sign {
return num > 0 ? 1 : -1;
}
function getInitValues(startPoint: Point, finalPoint: Point) {
const abs = Math.abs;
const diffx = finalPoint.x - startPoint.x;
const diffy = finalPoint.y - startPoint.y;
return {
absDiff: {
x: abs(diffx),
y: abs(diffy)
},
sign: {
x: getSing(diffx),
y: getSing(diffy)
}
};
}
function getBreakFn(sign: Sign): (current: number, final: number) => boolean {
return sign < 0
? (current, final) => current >= final
: (current, final) => current <= final;
}
function calcMainCoordinates(absDiff: Point): ["x", "y"] | ["y", "x"] {
return absDiff.x > absDiff.y ? ["x", "y"] : ["y", "x"];
}
export default function line(point: Point, finalPoint: Point) {
const { absDiff, sign } = getInitValues(point, finalPoint);
const [mainCoordinate, coordinate] = calcMainCoordinates(absDiff);
const final = finalPoint[mainCoordinate];
const mainSign = sign[mainCoordinate];
const secondSign = sign[coordinate];
const mainDiff = absDiff[mainCoordinate];
const secondDiff = absDiff[coordinate];
const breakFn = getBreakFn(mainSign);
let mainValue = point[mainCoordinate];
let secondValue = point[coordinate];
let eps = 0;
const points = [];
for (; breakFn(mainValue, final); mainValue += mainSign) {
points.push({
[mainCoordinate]: mainValue,
[coordinate]: secondValue
});
eps += secondDiff;
if (eps << 1 >= mainDiff) {
secondValue += secondSign;
eps -= mainDiff;
}
}
return points;
}

View file

@ -1,79 +1,104 @@
/*! Curve calc function for canvas 2.3.1
* Epistemex (c) 2013-2014
* License: MIT
*/
// Adapted from https://github.com/morganherlocker/cubic-spline
/**
* Calculates an array containing points representing a cardinal spline through given point array.
* Points must be arranged as: [x1, y1, x2, y2, ..., xn, yn].
*
* The points for the cardinal spline are returned as a new array.
*
* @param {Array} points - point array
* @param {Number} [tension=0.5] - tension. Typically between [0.0, 1.0] but can be exceeded
* @param {Number} [numOfSeg=20] - number of segments between two points (line resolution)
* @returns {Float32Array} New array with the calculated points that was added to the path
*/
export function getCurvePoints(points, tension = 0.5, numOfSeg = 25) {
let i = 1,
l = points.length,
rPos = 0,
cachePtr = 4;
const rLen = (l - 2) * numOfSeg + 2,
res = new Float32Array(rLen),
cache = new Float32Array((numOfSeg + 2) * 4);
// for cloning point array
const pts = points.slice(0);
export default function spline(x, xs, ys) {
let ks = xs.map(() => {
return 0;
});
ks = getNaturalKs(xs, ys, ks);
let i = 1;
while (xs[i] < x) i++;
const t = (x - xs[i - 1]) / (xs[i] - xs[i - 1]);
const a = ks[i - 1] * (xs[i] - xs[i - 1]) - (ys[i] - ys[i - 1]);
const b = -ks[i] * (xs[i] - xs[i - 1]) + (ys[i] - ys[i - 1]);
const q =
(1 - t) * ys[i - 1] + t * ys[i] + t * (1 - t) * (a * (1 - t) + b * t);
return q;
}
pts.unshift(points[1]); // copy 1. point and insert at beginning
pts.unshift(points[0]);
pts.push(points[l - 2], points[l - 1]); // duplicate end-points
function getNaturalKs(xs, ys, ks) {
const n = xs.length - 1;
const A = zerosMat(n + 1, n + 2);
// cache inner-loop calculations as they are based on t alone
cache[0] = 1; // 1,0,0,0
for (; i < numOfSeg; i++) {
const st = i / numOfSeg,
st2 = st * st,
st3 = st2 * st,
st23 = st3 * 2,
st32 = st2 * 3;
cache[cachePtr++] = st23 - st32 + 1; // c1
cache[cachePtr++] = st32 - st23; // c2
cache[cachePtr++] = st3 - 2 * st2 + st; // c3
cache[cachePtr++] = st3 - st2; // c4
for (
let i = 1;
i < n;
i++ // rows
) {
A[i][i - 1] = 1 / (xs[i] - xs[i - 1]);
A[i][i] = 2 * (1 / (xs[i] - xs[i - 1]) + 1 / (xs[i + 1] - xs[i]));
A[i][i + 1] = 1 / (xs[i + 1] - xs[i]);
A[i][n + 1] =
3 *
((ys[i] - ys[i - 1]) / ((xs[i] - xs[i - 1]) * (xs[i] - xs[i - 1])) +
(ys[i + 1] - ys[i]) / ((xs[i + 1] - xs[i]) * (xs[i + 1] - xs[i])));
}
cache[++cachePtr] = 1; // 0,1,0,0
A[0][0] = 2 / (xs[1] - xs[0]);
A[0][1] = 1 / (xs[1] - xs[0]);
A[0][n + 1] = (3 * (ys[1] - ys[0])) / ((xs[1] - xs[0]) * (xs[1] - xs[0]));
// calc. points
for (let j = 2, t; j < l; j += 2) {
const pt1 = pts[j],
pt2 = pts[j + 1],
pt3 = pts[j + 2],
pt4 = pts[j + 3],
t1x = (pt3 - pts[j - 2]) * tension,
t1y = (pt4 - pts[j - 1]) * tension,
t2x = (pts[j + 4] - pt1) * tension,
t2y = (pts[j + 5] - pt2) * tension;
A[n][n - 1] = 1 / (xs[n] - xs[n - 1]);
A[n][n] = 2 / (xs[n] - xs[n - 1]);
A[n][n + 1] =
(3 * (ys[n] - ys[n - 1])) / ((xs[n] - xs[n - 1]) * (xs[n] - xs[n - 1]));
for (t = 0; t < numOfSeg; t++) {
const c = t << 2, //t * 4;
c1 = cache[c],
c2 = cache[c + 1],
c3 = cache[c + 2],
c4 = cache[c + 3];
return solve(A, ks);
}
res[rPos++] = c1 * pt1 + c2 * pt3 + c3 * t1x + c4 * t2x;
res[rPos++] = c1 * pt2 + c2 * pt4 + c3 * t1y + c4 * t2y;
function solve(A, ks) {
const m = A.length;
for (
let k = 0;
k < m;
k++ // column
) {
// pivot for column
let i_max = 0;
let vali = Number.NEGATIVE_INFINITY;
for (var i = k; i < m; i++)
if (A[i][k] > vali) {
i_max = i;
vali = A[i][k];
}
swapRows(A, k, i_max);
// for all rows below pivot
for (var i = k + 1; i < m; i++) {
for (var j = k + 1; j < m + 1; j++)
A[i][j] = A[i][j] - A[k][j] * (A[i][k] / A[k][k]);
A[i][k] = 0;
}
}
// add last point
l = points.length - 2;
res[rPos++] = points[l];
res[rPos] = points[l + 1];
return res;
for (
var i = m - 1;
i >= 0;
i-- // rows = columns
) {
const v = A[i][m] / A[i][i];
ks[i] = v;
for (
var j = i - 1;
j >= 0;
j-- // rows
) {
A[j][m] -= A[j][i] * v;
A[j][i] = 0;
}
}
return ks;
}
function zerosMat(r, c) {
const A = [];
for (let i = 0; i < r; i++) {
A.push([]);
for (let j = 0; j < c; j++) A[i].push(0);
}
return A;
}
function swapRows(m, k, l) {
const p = m[k];
m[k] = m[l];
m[l] = p;
}