Skip to content

Commit 0fad5f0

Browse files
authored
Add extrapolation to interpolatePaths() (#662)
1 parent 7b3e271 commit 0fad5f0

File tree

3 files changed

+142
-14
lines changed

3 files changed

+142
-14
lines changed

package/src/animation/functions/interpolate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ function isExtrapolate(value: string): value is Extrapolate {
6060

6161
// validates extrapolations type
6262
// if type is correct, converts it to ExtrapolationConfig
63-
function validateType(type: ExtrapolationType): RequiredExtrapolationConfig {
63+
export function validateInterpolationOptions(
64+
type: ExtrapolationType
65+
): RequiredExtrapolationConfig {
6466
// initialize extrapolationConfig with default extrapolation
6567
const extrapolationConfig: RequiredExtrapolationConfig = {
6668
extrapolateLeft: Extrapolate.EXTEND,
@@ -151,7 +153,7 @@ export function interpolate(
151153
);
152154
}
153155

154-
const extrapolationConfig = validateType(type);
156+
const extrapolationConfig = validateInterpolationOptions(type);
155157
const { length } = input;
156158
const narrowedInput: InterpolationNarrowedInput = {
157159
leftEdgeInput: input[0],
Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
import type { SkPath } from "../../skia/types";
2+
import { exhaustiveCheck } from "../../renderer/typeddash";
3+
4+
import type { ExtrapolationType } from "./interpolate";
5+
import { validateInterpolationOptions, Extrapolate } from "./interpolate";
6+
7+
const lerp = (
8+
value: number,
9+
from: number,
10+
to: number,
11+
p1: SkPath,
12+
p2: SkPath
13+
) => {
14+
const t = (value - from) / (to - from);
15+
return p2.interpolate(p1, t)!;
16+
};
217

318
/**
419
* Maps an input value within a range to an output path within a path range.
520
* @param value - The input value.
621
* @param inputRange - The range of the input value.
722
* @param outputRange - The range of the output path.
23+
* @param options - Extrapolation options
824
* @returns The output path.
925
* @example <caption>Map a value between 0 and 1 to a path between two paths.</caption>
1026
* const path1 = new Path();
@@ -18,21 +34,54 @@ import type { SkPath } from "../../skia/types";
1834
export const interpolatePaths = (
1935
value: number,
2036
input: number[],
21-
outputRange: SkPath[]
37+
outputRange: SkPath[],
38+
options?: ExtrapolationType
2239
) => {
40+
const extrapolation = validateInterpolationOptions(options);
41+
if (value < input[0]) {
42+
switch (extrapolation.extrapolateLeft) {
43+
case Extrapolate.CLAMP:
44+
return outputRange[0];
45+
case Extrapolate.EXTEND:
46+
return lerp(value, input[0], input[1], outputRange[0], outputRange[1]);
47+
case Extrapolate.IDENTITY:
48+
throw new Error(
49+
"Identity is not a supported extrapolation type for interpolatePaths()"
50+
);
51+
default:
52+
exhaustiveCheck(extrapolation.extrapolateLeft);
53+
}
54+
} else if (value > input[input.length - 1]) {
55+
switch (extrapolation.extrapolateRight) {
56+
case Extrapolate.CLAMP:
57+
return outputRange[outputRange.length - 1];
58+
case Extrapolate.EXTEND:
59+
return lerp(
60+
value,
61+
input[input.length - 2],
62+
input[input.length - 1],
63+
outputRange[input.length - 2],
64+
outputRange[input.length - 1]
65+
);
66+
case Extrapolate.IDENTITY:
67+
throw new Error(
68+
"Identity is not a supported extrapolation type for interpolatePaths()"
69+
);
70+
default:
71+
exhaustiveCheck(extrapolation.extrapolateRight);
72+
}
73+
}
2374
let i = 0;
2475
for (; i <= input.length - 1; i++) {
2576
if (value >= input[i] && value <= input[i + 1]) {
2677
break;
2778
}
28-
if (i === input.length - 1) {
29-
if (value < input[0]) {
30-
return outputRange[0];
31-
} else {
32-
return outputRange[i];
33-
}
34-
}
3579
}
36-
const t = (value - input[i]) / (input[i + 1] - input[i]);
37-
return outputRange[i + 1].interpolate(outputRange[i], t)!;
80+
return lerp(
81+
value,
82+
input[i],
83+
input[i + 1],
84+
outputRange[i],
85+
outputRange[i + 1]
86+
);
3887
};

package/src/skia/__tests__/Path.spec.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ describe("Path", () => {
185185
const path3 = Skia.Path.Make();
186186
path3.moveTo(0, 0);
187187
path3.lineTo(200, 200);
188-
const path = interpolatePaths(-1, [0, 0.5, 1], [path1, path2, path3]);
188+
const path = interpolatePaths(
189+
-1,
190+
[0, 0.5, 1],
191+
[path1, path2, path3],
192+
"clamp"
193+
);
189194
expect(path.toCmds().flat()).toEqual(path1.toCmds().flat());
190195
});
191196

@@ -200,7 +205,12 @@ describe("Path", () => {
200205
const path3 = Skia.Path.Make();
201206
path3.moveTo(0, 0);
202207
path3.lineTo(200, 200);
203-
const path = interpolatePaths(2, [0, 0.5, 1], [path1, path2, path3]);
208+
const path = interpolatePaths(
209+
2,
210+
[0, 0.5, 1],
211+
[path1, path2, path3],
212+
"clamp"
213+
);
204214
expect(path.toCmds()).toEqual(path3.toCmds());
205215
});
206216

@@ -300,4 +310,71 @@ describe("Path", () => {
300310
}
301311
processResult(surface, "snapshots/path/interpolate.png");
302312
});
313+
314+
it("should support overshooting values in path interpolation", () => {
315+
const { Skia } = setupSkia();
316+
const p1 = Skia.Path.Make();
317+
p1.moveTo(0, 0);
318+
p1.lineTo(100, 100);
319+
320+
const p2 = Skia.Path.Make();
321+
p2.moveTo(0, 100);
322+
p2.lineTo(100, 0);
323+
324+
const p3 = p2.interpolate(p1, 1.1)!;
325+
expect(p3).not.toBeNull();
326+
const [[, moveX, moveY], [, lineX, lineY]] = p3.toCmds();
327+
expect(moveX).toBe(0);
328+
expect(moveY).toBe(110);
329+
expect(lineX).toBe(100);
330+
expect(lineY).toBe(-10);
331+
332+
const p4 = p2.interpolate(p1, -0.1)!;
333+
expect(p4).not.toBeNull();
334+
const [[, moveX1, moveY1], [, lineX1, lineY1]] = p4.toCmds();
335+
expect(moveX1).toBe(0);
336+
expect(moveY1).toBe(-10);
337+
expect(lineX1).toBe(100);
338+
expect(lineY1).toBe(110);
339+
});
340+
341+
it("interpolatePath() should support overshooting values", () => {
342+
const { Skia } = setupSkia();
343+
const p1 = Skia.Path.Make();
344+
p1.moveTo(0, 0);
345+
p1.lineTo(100, 100);
346+
347+
const p2 = Skia.Path.Make();
348+
p2.moveTo(0, 100);
349+
p2.lineTo(100, 0);
350+
351+
const ref1 = Skia.Path.Make();
352+
ref1.moveTo(0, -10);
353+
ref1.lineTo(100, 110);
354+
355+
const ref2 = Skia.Path.Make();
356+
ref2.moveTo(0, 110);
357+
ref2.lineTo(100, -10);
358+
359+
const p3 = interpolatePaths(-0.1, [0, 1], [p1, p2]);
360+
expect(p3.toCmds()).toEqual(ref1.toCmds());
361+
const p4 = interpolatePaths(1.1, [0, 1], [p1, p2]);
362+
expect(p4.toCmds()).toEqual(ref2.toCmds());
363+
});
364+
365+
it("interpolatePath() should support clamping left and right values", () => {
366+
const { Skia } = setupSkia();
367+
const p1 = Skia.Path.Make();
368+
p1.moveTo(0, 0);
369+
p1.lineTo(100, 100);
370+
371+
const p2 = Skia.Path.Make();
372+
p2.moveTo(0, 100);
373+
p2.lineTo(100, 0);
374+
375+
const p3 = interpolatePaths(-0.1, [0, 1], [p1, p2], "clamp");
376+
expect(p3.toCmds()).toEqual(p1.toCmds());
377+
const p4 = interpolatePaths(1.1, [0, 1], [p1, p2], "clamp");
378+
expect(p4.toCmds()).toEqual(p2.toCmds());
379+
});
303380
});

0 commit comments

Comments
 (0)