Skip to content

Commit

Permalink
1) moves code to create plots into Plot class, 2) fixes TangentLine
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryan Parker committed Apr 30, 2024
1 parent 5d8c9a4 commit a44c85d
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 171 deletions.
106 changes: 4 additions & 102 deletions src/shapes/composed/axes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,104 +196,7 @@ class Axes extends ComposedShape {
}

plot(fn: (x: number) => number | null): Plot {
const stepSize = (this._xRange[1] - this._xRange[0]) / MIN_NUM_STEPS;
// const origin = this.origin();

let lastValidPoint: Point | null = null;
const [yLow, yHigh] = this._yRange;

const interpolate = (x: number, y: number, lastY: number, targetY: number): Point | null => {
if (lastValidPoint === null) {
return null;
}

const [lastX, _] = lastValidPoint;
let slope = (y - lastY) / (x - lastX);

if (slope === 0) return null;

let b = lastY - (slope * lastX);
let interpolatedX = (targetY - b) / slope;

if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) {
return [interpolatedX, targetY];
} else {
return null;
}
};

const origFn = (x: number) => {
const y = fn(x);
return [x, y] as [number, number | null];
};

const plot = new Plot(origFn);
let segmentPoints: Point[] = [];

const finalizeSegment = () => {
if (segmentPoints.length > 0) {
const transformedPoints = segmentPoints.map(pt => {
// const [pX, pY] = math.addVec(pt, origin);
return this.point(...pt);
});

plot.addSubplot(new PointShape({ points: transformedPoints, smooth: true, }));
segmentPoints = [];
}
};

const addPoint = (x: number) => {
let y = fn(x);

// TODO: add hole
if (!Number.isFinite(y) || y === null) {
lastValidPoint = null;
finalizeSegment();
return;
}

if (y >= yLow && y <= yHigh) {
/* The point is within the Y-range, but the previous point was outside. In order to avoid leaving a gap between the top of the Y-range and the
* start of the plot, we interpolate a point between the last valid point and the current point. */
if (lastValidPoint === null) {
// Last point was invalid
const lastX = x - stepSize;
const lastY = fn(lastX);

if (lastY !== null) {
const tY = lastY > yHigh ? yHigh : yLow;
const slope = (y - lastY) / stepSize;
let b = lastY - (slope * lastX);
let interpolatedX = (tY - b) / slope;
if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) {
segmentPoints.push([interpolatedX, tY] as Point);
}
}
}

const point = [x, y] as Point;
segmentPoints.push(point);
lastValidPoint = point;
} else if (lastValidPoint) {
const tY = y > yHigh ? yHigh : yLow;
const interPoint = interpolate(x, y, lastValidPoint[1], tY);
if (interPoint) {
segmentPoints.push(interPoint);
}
finalizeSegment();
lastValidPoint = null;
}
};

for (let x = this._xRange[0]; x <= this._xRange[1]; x += stepSize) {
addPoint(x);
}

// If the step size doesn't land directly on the end of the range, add the last point in the range
addPoint(this._xRange[1]);
finalizeSegment();

return plot;
return new Plot(fn, this._xRange, this._yRange, this.point.bind(this));
}

area(plot: Plot, range?: [number, number]): Shape {
Expand All @@ -302,10 +205,9 @@ class Axes extends ComposedShape {
const points = [this.point(rS, 0)];
const stepSize = (rE - rS) / MIN_NUM_STEPS;
for (let x = rS; x <= rE; x += stepSize) {
const y = plot.valueAtX(x);

if (y !== null) {
points.push(this.point(x, y));
const pt = plot.pointAtX(x);
if (pt) {
points.push(pt);
}
}

Expand Down
127 changes: 110 additions & 17 deletions src/shapes/composed/plot.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,133 @@
import { BezierSegment } from '@/base';
import { BezierSegment, Point } from '@/base';
import { ComposedShape } from './composed_shape';
import { PointShape } from '@/shapes/primitives/point_shape';


// The number of discrete points that make up the plot function
const MIN_NUM_STEPS = 200;


class Plot extends ComposedShape {
private _subplots: PointShape[] = [];
private _fn: (x: number) => [number, number | null];
// private _subplots: PointShape[] = [];
private _fn: (x: number) => number | null;
private _xRange: [number, number];
private _yRange: [number, number];
private _pointTransformer: (x: number, y: number) => Point;

constructor(fn: (x: number) => [number, number | null]) {
constructor(fn: (x: number) => number | null, xRange: [number, number], yRange: [number, number], pointTransformer: (x: number, y: number) => Point) {
super();
this._fn = fn;
}

addSubplot(shape: PointShape): void {
this._subplots.push(shape);
this._xRange = xRange;
this._yRange = yRange;
this._pointTransformer = pointTransformer;
}

compose(): this {
for (const subplot of this._subplots) {
this.add(subplot);
const stepSize = (this._xRange[1] - this._xRange[0]) / MIN_NUM_STEPS;
const [yLow, yHigh] = this._yRange;

let lastValidPoint: Point | null = null;

const interpolate = (x: number, y: number, lastY: number, targetY: number): Point | null => {
if (lastValidPoint === null) {
return null;
}

const [lastX, _] = lastValidPoint;
let slope = (y - lastY) / (x - lastX);

if (slope === 0) return null;

let b = lastY - (slope * lastX);
let interpolatedX = (targetY - b) / slope;

if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) {
return [interpolatedX, targetY];
} else {
return null;
}
};

let segmentPoints: Point[] = [];

const finalizeSegment = () => {
if (segmentPoints.length > 0) {
const transformedPoints = segmentPoints.map(pt => this._pointTransformer(pt[0], pt[1]));
this.add(new PointShape({ points: transformedPoints, smooth: true, }));
segmentPoints = [];
}
};

const addPoint = (x: number) => {
let y = this._fn(x);

// TODO: add hole
if (!Number.isFinite(y) || y === null) {
lastValidPoint = null;
finalizeSegment();
return;
}

if (y >= yLow && y <= yHigh) {
/* The point is within the Y-range, but the previous point was outside. In order to avoid leaving a gap between the top of the Y-range and the
* start of the plot, we interpolate a point between the last valid point and the current point. */
if (lastValidPoint === null) {
// Last point was invalid
const lastX = x - stepSize;
const lastY = this._fn(lastX);

if (lastY !== null) {
const tY = lastY > yHigh ? yHigh : yLow;
const slope = (y - lastY) / stepSize;
let b = lastY - (slope * lastX);
let interpolatedX = (tY - b) / slope;
if (interpolatedX >= this._xRange[0] && interpolatedX <= this._xRange[1]) {
segmentPoints.push([interpolatedX, tY] as Point);
}
}
}

const point = [x, y] as Point;
segmentPoints.push(point);
lastValidPoint = point;
} else if (lastValidPoint) {
const tY = y > yHigh ? yHigh : yLow;
const interPoint = interpolate(x, y, lastValidPoint[1], tY);
if (interPoint) {
segmentPoints.push(interPoint);
}
finalizeSegment();
lastValidPoint = null;
}
};

for (let x = this._xRange[0]; x <= this._xRange[1]; x += stepSize) {
addPoint(x);
}

// If the step size doesn't land directly on the end of the range, add the last point in the range
addPoint(this._xRange[1]);
finalizeSegment();

return this;
}

valueAtX(x: number): number | null {
return this._fn(x)[1];
return this._fn(x);
}

*points(): Generator<BezierSegment> {
for (const subplot of this._subplots) {
for (const point of subplot.points()) {
yield point;
}
}
pointAtX(x: number): Point | null {
const y = this._fn(x);
return y !== null ? this._pointTransformer(x, y) : null;
}

// *points(): Generator<BezierSegment> {
// for (const subplot of this._subplots) {
// for (const point of subplot.points()) {
// yield point;
// }
// }
// }
}


Expand Down
30 changes: 18 additions & 12 deletions src/shapes/composed/tangent_line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,30 @@ class TangentLine extends ComposedShape {
}

compose(): this {
// Before transforming the points, we need to calculate the slope
const y1 = this._plot.valueAtX(this._x);
const y2 = this._plot.valueAtX(this._x + this._delta);

if (y1 !== null && y2 !== null) {
const m = (y2 - y1) / this._delta;
if (y1 === null || y2 === null) {
// Find other points?
return this;
}

const from = [
this._x + (this._length / (2 * Math.sqrt(1 + m ** 2))),
m * (this._length / (2 * Math.sqrt(1 + m ** 2))) + y1
] as Point;
const m = (y2 - y1) / this._delta;

const to = [
this._x - (this._length / (2 * Math.sqrt(1 + m ** 2))),
y1 - m * (this._length / (2 * Math.sqrt(1 + m ** 2)))
] as Point;
const [tx1, ty1] = this._plot.pointAtX(this._x)!;

this.add(new Line({ from, to, ...this.styles() }));
}
const from = [
tx1 - (this._length / (2 * Math.sqrt(1 + m ** 2))),
ty1 - m * (this._length / (2 * Math.sqrt(1 + m ** 2)))
] as Point;

const to = [
tx1 + (this._length / (2 * Math.sqrt(1 + m ** 2))),
m * (this._length / (2 * Math.sqrt(1 + m ** 2))) + ty1
] as Point;

this.add(new Line({ from, to, ...this.styles() }));

return this;
}
Expand Down
20 changes: 13 additions & 7 deletions src/shapes/derived/circle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ORIGIN, Prettify } from '@/base';
import { Arc } from '../primitives/arc';
import { Locatable, ShapeStyles } from '@/shapes/shape';
import { Locatable, ShapeStyles, isLocatable } from '@/shapes/shape';


type CircleArgs = { center?: Locatable; radius?: number; selectable?: boolean; };
Expand All @@ -13,13 +13,19 @@ class Circle extends Arc {
constructor(radius: number);
constructor(args: Prettify<CircleArgs & ShapeStyles>);
constructor(args?: Locatable | number | Prettify<CircleArgs & ShapeStyles>) {
if (args === undefined) {
super(defaultCircleArgs);
} else if (typeof args === 'number') {
super(Object.assign({}, defaultCircleArgs, { radius: args }));
} else {
super(Object.assign({}, defaultCircleArgs, args));
let circleArgs = structuredClone(defaultCircleArgs);

if (args !== undefined) {
if (isLocatable(args)) {
circleArgs.center = args;
} else if (typeof args === 'number') {
circleArgs.radius = args;
} else {
circleArgs = { ...circleArgs, ...args };
}
}

super(circleArgs);
}
}

Expand Down
Loading

0 comments on commit a44c85d

Please sign in to comment.