-
Notifications
You must be signed in to change notification settings - Fork 483
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Path Commands (#422)
- Loading branch information
1 parent
705d977
commit ab912f7
Showing
13 changed files
with
7,645 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,4 +52,4 @@ | |
"node" | ||
] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
import type { Vector, PathCommand } from "@shopify/react-native-skia"; | ||
import { | ||
PathVerb, | ||
vec, | ||
Skia, | ||
cartesian2Polar, | ||
} from "@shopify/react-native-skia"; | ||
import { exhaustiveCheck } from "@shopify/react-native-skia/src/renderer/typeddash"; | ||
|
||
import type { Cubic } from "../Aurora/components/Cubic"; | ||
|
||
const round = (value: number, precision = 0) => { | ||
const p = Math.pow(10, precision); | ||
return Math.round(value * p) / p; | ||
}; | ||
|
||
// https://stackoverflow.com/questions/27176423/function-to-solve-cubic-equation-analytically | ||
const cuberoot = (x: number) => { | ||
const y = Math.pow(Math.abs(x), 1 / 3); | ||
return x < 0 ? -y : y; | ||
}; | ||
|
||
const solveCubic = (a: number, b: number, c: number, d: number) => { | ||
if (Math.abs(a) < 1e-8) { | ||
// Quadratic case, ax^2+bx+c=0 | ||
a = b; | ||
b = c; | ||
c = d; | ||
if (Math.abs(a) < 1e-8) { | ||
// Linear case, ax+b=0 | ||
a = b; | ||
b = c; | ||
if (Math.abs(a) < 1e-8) { | ||
// Degenerate case | ||
return []; | ||
} | ||
return [-b / a]; | ||
} | ||
|
||
const D = b * b - 4 * a * c; | ||
if (Math.abs(D) < 1e-8) { | ||
return [-b / (2 * a)]; | ||
} else if (D > 0) { | ||
return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)]; | ||
} | ||
return []; | ||
} | ||
|
||
// Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a) | ||
const p = (3 * a * c - b * b) / (3 * a * a); | ||
const q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a); | ||
let roots; | ||
|
||
if (Math.abs(p) < 1e-8) { | ||
// p = 0 -> t^3 = -q -> t = -q^1/3 | ||
roots = [cuberoot(-q)]; | ||
} else if (Math.abs(q) < 1e-8) { | ||
// q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0 | ||
roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []); | ||
} else { | ||
const D = (q * q) / 4 + (p * p * p) / 27; | ||
if (Math.abs(D) < 1e-8) { | ||
// D = 0 -> two roots | ||
roots = [(-1.5 * q) / p, (3 * q) / p]; | ||
} else if (D > 0) { | ||
// Only one real root | ||
const u = cuberoot(-q / 2 - Math.sqrt(D)); | ||
roots = [u - p / (3 * u)]; | ||
} else { | ||
// D < 0, three roots, but needs to use complex numbers/trigonometric solution | ||
const u = 2 * Math.sqrt(-p / 3); | ||
const t = Math.acos((3 * q) / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1] | ||
const k = (2 * Math.PI) / 3; | ||
roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)]; | ||
} | ||
} | ||
|
||
// Convert back from depressed cubic | ||
for (let i = 0; i < roots.length; i++) { | ||
roots[i] -= b / (3 * a); | ||
} | ||
|
||
return roots; | ||
}; | ||
|
||
export const cubicBezierYForX = ( | ||
x: number, | ||
a: Vector, | ||
b: Vector, | ||
c: Vector, | ||
d: Vector, | ||
precision = 2 | ||
) => { | ||
const pa = -a.x + 3 * b.x - 3 * c.x + d.x; | ||
const pb = 3 * a.x - 6 * b.x + 3 * c.x; | ||
const pc = -3 * a.x + 3 * b.x; | ||
const pd = a.x - x; | ||
const t = solveCubic(pa, pb, pc, pd) | ||
.map((root) => round(root, precision)) | ||
.filter((root) => root >= 0 && root <= 1)[0]; | ||
return cubicBezier(t, a.y, b.y, c.y, d.y); | ||
}; | ||
|
||
const cubicBezier = ( | ||
t: number, | ||
from: number, | ||
c1: number, | ||
c2: number, | ||
to: number | ||
) => { | ||
const term = 1 - t; | ||
const a = 1 * term ** 3 * t ** 0 * from; | ||
const b = 3 * term ** 2 * t ** 1 * c1; | ||
const c = 3 * term ** 1 * t ** 2 * c2; | ||
const d = 1 * term ** 0 * t ** 3 * to; | ||
return a + b + c + d; | ||
}; | ||
|
||
interface Cubic { | ||
from: Vector; | ||
c1: Vector; | ||
c2: Vector; | ||
to: Vector; | ||
} | ||
|
||
export const selectCurve = (cmds: PathCommand[], x: number): Cubic | null => { | ||
for (let i = 0; i < cmds.length; i++) { | ||
const cmd = cmds[i]; | ||
|
||
if (cmd[0] === PathVerb.Cubic) { | ||
const to = vec(cmd[1], cmd[2]); | ||
const from = vec(cmd[7], cmd[8]); | ||
if (x >= from.x && x <= to.x) { | ||
return { | ||
from, | ||
c1: vec(cmd[3], cmd[4]), | ||
c2: vec(cmd[5], cmd[6]), | ||
to, | ||
}; | ||
} | ||
} | ||
} | ||
return null; | ||
}; | ||
|
||
export const getYForX = (cmds: PathCommand[], x: number, precision = 2) => { | ||
const c = selectCurve(cmds, x); | ||
if (c === null) { | ||
return null; | ||
} | ||
return cubicBezierYForX(x, c.from, c.c1, c.c2, c.to, precision); | ||
}; | ||
|
||
export const controlPoint = ( | ||
current: Vector, | ||
previous: Vector, | ||
next: Vector, | ||
reverse: boolean, | ||
smoothing: number | ||
) => { | ||
const p = previous || current; | ||
const n = next || current; | ||
// Properties of the opposed-line | ||
const lengthX = n.x - p.x; | ||
const lengthY = n.y - p.y; | ||
|
||
const o = cartesian2Polar({ x: lengthX, y: lengthY }); | ||
// If is end-control-point, add PI to the angle to go backward | ||
const angle = o.theta + (reverse ? Math.PI : 0); | ||
const length = o.radius * smoothing; | ||
// The control point position is relative to the current point | ||
const x = current.x + Math.cos(angle) * length; | ||
const y = current.y + Math.sin(angle) * length; | ||
return { x, y }; | ||
}; | ||
|
||
export const curveLines = ( | ||
points: Vector[], | ||
smoothing: number, | ||
strategy: "complex" | "bezier" | "simple" | ||
) => { | ||
const path = Skia.Path.Make(); | ||
path.moveTo(points[0].x, points[0].y); | ||
// build the d attributes by looping over the points | ||
for (let i = 0; i < points.length; i++) { | ||
if (i === 0) { | ||
continue; | ||
} | ||
const point = points[i]; | ||
const next = points[i + 1]; | ||
const prev = points[i - 1]; | ||
const cps = controlPoint(prev, points[i - 2], point, false, smoothing); | ||
const cpe = controlPoint(point, prev, next, true, smoothing); | ||
switch (strategy) { | ||
case "simple": | ||
const cp = { | ||
x: (cps.x + cpe.x) / 2, | ||
y: (cps.y + cpe.y) / 2, | ||
}; | ||
path.quadTo(cp.x, cp.y, point.x, point.y); | ||
break; | ||
case "bezier": | ||
const p0 = points[i - 2] || prev; | ||
const p1 = points[i - 1]; | ||
const cp1x = (2 * p0.x + p1.x) / 3; | ||
const cp1y = (2 * p0.y + p1.y) / 3; | ||
const cp2x = (p0.x + 2 * p1.x) / 3; | ||
const cp2y = (p0.y + 2 * p1.y) / 3; | ||
const cp3x = (p0.x + 4 * p1.x + point.x) / 6; | ||
const cp3y = (p0.y + 4 * p1.y + point.y) / 6; | ||
path.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y); | ||
if (i === points.length - 1) { | ||
path.cubicTo( | ||
points[points.length - 1].x, | ||
points[points.length - 1].y, | ||
points[points.length - 1].x, | ||
points[points.length - 1].y, | ||
points[points.length - 1].x, | ||
points[points.length - 1].y | ||
); | ||
} | ||
break; | ||
case "complex": | ||
path.cubicTo(cps.x, cps.y, cpe.x, cpe.y, point.x, point.y); | ||
break; | ||
default: | ||
exhaustiveCheck(strategy); | ||
} | ||
} | ||
return path; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/* eslint-disable camelcase */ | ||
import { Dimensions } from "react-native"; | ||
|
||
import data from "./data.json"; | ||
import { curveLines } from "./Math"; | ||
|
||
export const SIZE = Dimensions.get("window").width; | ||
export const PADDING = 16; | ||
|
||
interface Amount { | ||
amount: string; | ||
currency: string; | ||
scale: string; | ||
} | ||
|
||
interface PercentChange { | ||
hour: number; | ||
day: number; | ||
week: number; | ||
month: number; | ||
year: number; | ||
} | ||
|
||
interface LatestPrice { | ||
amount: Amount; | ||
timestamp: string; | ||
percent_change: PercentChange; | ||
} | ||
|
||
type PriceList = [string, number][]; | ||
|
||
interface DataPoints { | ||
percent_change: number; | ||
prices: PriceList; | ||
} | ||
|
||
interface Prices { | ||
latest: string; | ||
latest_price: LatestPrice; | ||
hour: DataPoints; | ||
day: DataPoints; | ||
week: DataPoints; | ||
month: DataPoints; | ||
year: DataPoints; | ||
all: DataPoints; | ||
} | ||
|
||
const values = data.data.prices as Prices; | ||
const POINTS = 60; | ||
|
||
const buildGraph = (datapoints: DataPoints, label: string) => { | ||
const priceList = datapoints.prices.slice(0, POINTS); | ||
const formattedValues = priceList.map( | ||
(price) => [parseFloat(price[0]), price[1]] as [number, number] | ||
); | ||
const prices = formattedValues.map((value) => value[0]); | ||
const dates = formattedValues.map((value) => value[1]); | ||
const minDate = Math.min(...dates); | ||
const maxDate = Math.max(...dates); | ||
const minPrice = Math.min(...prices); | ||
const maxPrice = Math.max(...prices); | ||
const AJUSTED_SIZE = SIZE - PADDING * 2; | ||
const points = formattedValues.map(([price, date]) => { | ||
const x = ((date - minDate) / (maxDate - minDate)) * SIZE; | ||
const y = ((price - minPrice) / (maxPrice - minPrice)) * AJUSTED_SIZE; | ||
return { x, y }; | ||
}); | ||
const path = curveLines(points, 0.1, "complex"); | ||
return { | ||
label, | ||
minPrice, | ||
maxPrice, | ||
percentChange: datapoints.percent_change, | ||
path, | ||
}; | ||
}; | ||
|
||
export const graphs = [ | ||
{ | ||
label: "1H", | ||
value: 0, | ||
data: buildGraph(values.hour, "Last Hour"), | ||
}, | ||
{ | ||
label: "1D", | ||
value: 1, | ||
data: buildGraph(values.day, "Today"), | ||
}, | ||
{ | ||
label: "1M", | ||
value: 2, | ||
data: buildGraph(values.month, "Last Month"), | ||
}, | ||
{ | ||
label: "1Y", | ||
value: 3, | ||
data: buildGraph(values.year, "This Year"), | ||
}, | ||
{ | ||
label: "all", | ||
value: 4, | ||
data: buildGraph(values.all, "All time"), | ||
}, | ||
] as const; | ||
|
||
export type GraphIndex = 0 | 1 | 2 | 3 | 4; |
Oops, something went wrong.