Draw eq graph as pixels

This commit is contained in:
Jordan Eldredge 2018-11-03 21:14:14 -07:00
parent 9abd9a9e36
commit 683cd69582
3 changed files with 218 additions and 29 deletions

View file

@ -1,10 +1,14 @@
import React from "react";
import { connect } from "react-redux";
import { getCurvePoints } from "cardinal-spline-js";
import { getCurvePoints } from "./spline";
import line from "./bresenham";
import { percentToRange, clamp } from "../../utils";
import { BANDS } from "../../constants";
const GRAPH_HEIGHT = 19;
const GRAPH_WIDTH = 113;
class EqGraph extends React.Component {
constructor(props) {
super(props);
@ -13,9 +17,8 @@ class EqGraph extends React.Component {
componentDidMount() {
this.canvasCtx = this.canvas.getContext("2d");
this.canvasCtx.imageSmoothingEnabled = false;
this.width = this.canvas.width * 1; // Cast to int
this.height = this.canvas.height * 1; // Cast to int
this.width = Number(this.canvas.width);
this.height = Number(this.canvas.height);
if (this.props.lineColorsImage) {
this.createColorPattern(this.props.lineColorsImage);
@ -52,17 +55,12 @@ class EqGraph extends React.Component {
createColorPattern(lineColorsImage) {
const bgImage = new Image();
bgImage.onload = () => {
const { width, height } = bgImage;
const colorsCanvas = document.createElement("canvas");
const colorsCtx = colorsCanvas.getContext("2d");
colorsCanvas.width = bgImage.width * 2;
colorsCanvas.height = bgImage.height * 2;
colorsCtx.drawImage(
bgImage,
0,
0,
colorsCanvas.width,
colorsCanvas.height
);
colorsCanvas.width = width;
colorsCanvas.height = height;
colorsCtx.drawImage(bgImage, 0, 0, width, height);
this.setState({
colorPattern: this.canvasCtx.createPattern(colorsCanvas, "repeat-x")
});
@ -78,31 +76,42 @@ class EqGraph extends React.Component {
const { props } = this;
const amplitudes = BANDS.map(band => props[band]);
this.canvasCtx.strokeStyle = this.state.colorPattern;
this.canvasCtx.lineWidth = 2;
this.canvasCtx.beginPath();
const paddingLeft = 4; // TODO: This should be 3
this.canvasCtx.fillStyle = this.state.colorPattern;
const paddingLeft = 2; // TODO: This should be 1.5
const min = 1;
const max = 31;
const min = 0;
const max = GRAPH_HEIGHT - 1;
const points = amplitudes.reduce((prev, value, i) => {
const percent = (100 - value) / 100;
const y = percentToRange(percent, min, max);
return prev.concat(paddingLeft + i * 16, y);
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.5;
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 y = clamp(smoothPoints[i + 1], min, max);
this.canvasCtx.lineTo(smoothPoints[i], y);
const x = Math.round(smoothPoints[i]);
const y = Math.round(clamp(smoothPoints[i + 1], min, max));
smoothPointCoords.push({ x, y });
}
this.canvasCtx.stroke();
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;
});
}
drawPreampLine() {
@ -111,13 +120,17 @@ class EqGraph extends React.Component {
// The skin has not finished loading yet
return;
}
const preampValue = percentToRange(this.props.preamp / 100, 0, 30);
const preampValue = percentToRange(
this.props.preamp / 100,
0,
GRAPH_HEIGHT - 1
);
this.canvasCtx.drawImage(
this.state.preampLineImg,
0,
preampValue,
preampLineImg.width * 2,
preampLineImg.height * 2
preampLineImg.width,
preampLineImg.height
);
}
@ -126,8 +139,8 @@ class EqGraph extends React.Component {
<canvas
id="eqGraph"
ref={node => (this.canvas = node)}
width="152"
height="32"
width={GRAPH_WIDTH}
height={GRAPH_HEIGHT}
/>
);
}

View file

@ -0,0 +1,71 @@
// 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

@ -0,0 +1,105 @@
/*! Curve calc function for canvas 2.3.1
* Epistemex (c) 2013-2014
* License: MIT
*/
/**
* 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)
* @param {Boolean} [close=false] - Close the ends making the line continuous
* @returns {Float32Array} New array with the calculated points that was added to the path
*/
export function getCurvePoints(points, tension, numOfSeg, close) {
"use strict";
// options or defaults
tension = typeof tension === "number" ? tension : 0.5;
numOfSeg = numOfSeg ? numOfSeg : 25;
var pts, // for cloning point array
i = 1,
l = points.length,
rPos = 0,
rLen = (l - 2) * numOfSeg + 2 + (close ? 2 * numOfSeg : 0),
res = new Float32Array(rLen),
cache = new Float32Array((numOfSeg + 2) * 4),
cachePtr = 4;
pts = points.slice(0);
if (close) {
pts.unshift(points[l - 1]); // insert end point as first point
pts.unshift(points[l - 2]);
pts.push(points[0], points[1]); // first point as last point
} else {
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
}
// cache inner-loop calculations as they are based on t alone
cache[0] = 1; // 1,0,0,0
for (; i < numOfSeg; i++) {
var 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
}
cache[++cachePtr] = 1; // 0,1,0,0
// calc. points
parse(pts, cache, l);
if (close) {
//l = points.length;
pts = [];
pts.push(points[l - 4], points[l - 3], points[l - 2], points[l - 1]); // second last and last
pts.push(points[0], points[1], points[2], points[3]); // first and second
parse(pts, cache, 4);
}
function parse(pts, cache, l) {
for (var i = 2, t; i < l; i += 2) {
var pt1 = pts[i],
pt2 = pts[i + 1],
pt3 = pts[i + 2],
pt4 = pts[i + 3],
t1x = (pt3 - pts[i - 2]) * tension,
t1y = (pt4 - pts[i - 1]) * tension,
t2x = (pts[i + 4] - pt1) * tension,
t2y = (pts[i + 5] - pt2) * tension;
for (t = 0; t < numOfSeg; t++) {
var c = t << 2, //t * 4;
c1 = cache[c],
c2 = cache[c + 1],
c3 = cache[c + 2],
c4 = cache[c + 3];
res[rPos++] = c1 * pt1 + c2 * pt3 + c3 * t1x + c4 * t2x;
res[rPos++] = c1 * pt2 + c2 * pt4 + c3 * t1y + c4 * t2y;
}
}
}
// add last point
l = close ? 0 : points.length - 2;
res[rPos++] = points[l];
res[rPos] = points[l + 1];
return res;
}