From 683cd69582c4b1ea43b1f109e462460ef0803799 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 3 Nov 2018 21:14:14 -0700 Subject: [PATCH] Draw eq graph as pixels --- js/components/EqualizerWindow/EqGraph.js | 71 ++++++++------ js/components/EqualizerWindow/bresenham.ts | 71 ++++++++++++++ js/components/EqualizerWindow/spline.js | 105 +++++++++++++++++++++ 3 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 js/components/EqualizerWindow/bresenham.ts create mode 100644 js/components/EqualizerWindow/spline.js diff --git a/js/components/EqualizerWindow/EqGraph.js b/js/components/EqualizerWindow/EqGraph.js index cc32551d..ec09db1c 100644 --- a/js/components/EqualizerWindow/EqGraph.js +++ b/js/components/EqualizerWindow/EqGraph.js @@ -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 { (this.canvas = node)} - width="152" - height="32" + width={GRAPH_WIDTH} + height={GRAPH_HEIGHT} /> ); } diff --git a/js/components/EqualizerWindow/bresenham.ts b/js/components/EqualizerWindow/bresenham.ts new file mode 100644 index 00000000..cfcf1927 --- /dev/null +++ b/js/components/EqualizerWindow/bresenham.ts @@ -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; +} diff --git a/js/components/EqualizerWindow/spline.js b/js/components/EqualizerWindow/spline.js new file mode 100644 index 00000000..2f13895a --- /dev/null +++ b/js/components/EqualizerWindow/spline.js @@ -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; +}