Skip to content

Commit

Permalink
Sample Array from Float Curve (#510)
Browse files Browse the repository at this point in the history
* sample array from float curve

* address comments
  • Loading branch information
mck authored Nov 12, 2024
1 parent a986fde commit aa21dae
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/lemon-spies-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/graph-engine": minor
---

Add Sample Array from Float Curve node that gives you n values from your float curve.
4 changes: 3 additions & 1 deletion packages/graph-engine/src/nodes/curves/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import flipFloatCurve from './flipFloatCurve.js';
import floatCurve from './floatCurve.js';
import presetBezier from './presetBeziers.js';
import sample from './sample.js';
import sampleFloatCurve from './sampleFloatCurve.js';

export const nodes = [
bezier,
Expand All @@ -13,5 +14,6 @@ export const nodes = [
sample,
deconstructFloatCurve,
flipFloatCurve,
constructFloatCurve
constructFloatCurve,
sampleFloatCurve
];
71 changes: 71 additions & 0 deletions packages/graph-engine/src/nodes/curves/sampleFloatCurve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { FloatCurve, INodeDefinition, ToInput, ToOutput } from '../../index.js';
import { FloatCurveSchema, NumberSchema } from '../../schemas/index.js';
import { Node } from '../../programmatic/node.js';
import { arrayOf } from '../../schemas/utils.js';
import { cubicBezier } from './floatCurve.js';
import { setToPrecision } from '../../utils/precision.js';

export default class NodeDefinition extends Node {
static title = 'Sample Array from Float Curve';
static type = 'studio.tokens.curve.sampleFloatCurve';
static description = 'Samples a float curve at given x values';

declare inputs: ToInput<{
curve: FloatCurve;
samplePoints: number[];
precision: number;
}>;
declare outputs: ToOutput<{
values: number[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('curve', {
type: FloatCurveSchema
});
this.addInput('samplePoints', {
type: arrayOf({
...NumberSchema,
minimum: 0,
maximum: 1,
description: 'X values to sample (between 0 and 1)'
})
});
this.addInput('precision', {
type: {
...NumberSchema,
minimum: 0,
default: 2
}
});
this.addOutput('values', {
type: arrayOf(NumberSchema)
});
}

execute(): void | Promise<void> {
const { curve, samplePoints, precision } = this.getAllInputs();
const values: number[] = [];

for (const x of samplePoints) {
const startIndex = curve.segments.findIndex((segment, idx) => {
const nextSegment = curve.segments[idx + 1];
return segment[0] <= x && (!nextSegment || x <= nextSegment[0]);
});

const p0 = curve.segments[startIndex];
const p1 = curve.segments[startIndex + 1];
const [c1, c2] = curve.controlPoints[startIndex];

const t = (x - p0[0]) / (p1[0] - p0[0]);
const y = setToPrecision(
cubicBezier(p0[1], c1[1], c2[1], p1[1], t),
precision
);
values.push(y);
}

this.outputs.values.set(values);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Graph } from '../../../../src/graph/graph.js';
import { describe, expect, test } from 'vitest';
import Node from '../../../../src/nodes/curves/sampleFloatCurve.js';

describe('curves/sampleFloatCurve', () => {
const sampleCurve = {
segments: [
[0, 0],
[0.5, 0.8],
[1, 1]
],
controlPoints: [
[
[0.2, 0.3],
[0.4, 0.7]
],
[
[0.6, 0.85],
[0.8, 0.95]
]
]
};

test('samples at given x values', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.curve.setValue(sampleCurve);
node.inputs.samplePoints.setValue([0, 0.25, 0.5, 0.75, 1]);

await node.execute();

const values = node.outputs.values.value;
expect(values).to.have.lengthOf(5);
expect(values[0]).to.equal(0); // Value at x=0
expect(values[2]).to.be.approximately(0.8, 0.1); // Value at x=0.5
expect(values[4]).to.equal(1); // Value at x=1
});

test('respects precision setting', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.curve.setValue(sampleCurve);
node.inputs.samplePoints.setValue([0, 0.5, 1]);
node.inputs.precision.setValue(1);

await node.execute();

const values = node.outputs.values.value;
values.forEach(value => {
const decimalPlaces = value.toString().split('.')[1]?.length || 0;
expect(decimalPlaces).to.be.lessThanOrEqual(1);
});
});

test('handles edge points', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.curve.setValue(sampleCurve);
node.inputs.samplePoints.setValue([0, 1]);

await node.execute();

const values = node.outputs.values.value;
expect(values).to.have.lengthOf(2);
expect(values[0]).to.equal(0); // Start point
expect(values[1]).to.equal(1); // End point
});

test('samples complex curve correctly', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.curve.setValue({
segments: [
[0, 1],
[0.3, 0.2],
[0.7, 0.8],
[1, 0]
],
controlPoints: [
[
[0.1, 0.8],
[0.2, 0.4]
],
[
[0.4, 0.3],
[0.6, 0.7]
],
[
[0.8, 0.6],
[0.9, 0.2]
]
]
});
node.inputs.samplePoints.setValue([0, 0.3, 0.7, 1]);

await node.execute();

const values = node.outputs.values.value;
expect(values).to.have.lengthOf(4);
expect(values[0]).to.equal(1); // Start value
expect(values[3]).to.equal(0); // End value
});
});

0 comments on commit aa21dae

Please sign in to comment.