@motionscript/plot 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/browser/chunks/chunk-RCFWYW6O.js +2 -0
- package/dist/browser/chunks/chunk-RCFWYW6O.js.map +7 -0
- package/dist/browser/index.js +84 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +12 -0
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +14 -0
- package/dist/engine.js.map +1 -0
- package/dist/graph2d/curve-cache.d.ts +60 -0
- package/dist/graph2d/curve-cache.d.ts.map +1 -0
- package/dist/graph2d/curve-cache.js +79 -0
- package/dist/graph2d/curve-cache.js.map +1 -0
- package/dist/graph2d/curve-error.d.ts +12 -0
- package/dist/graph2d/curve-error.d.ts.map +1 -0
- package/dist/graph2d/curve-error.js +15 -0
- package/dist/graph2d/curve-error.js.map +1 -0
- package/dist/graph2d/curve.d.ts +104 -0
- package/dist/graph2d/curve.d.ts.map +1 -0
- package/dist/graph2d/curve.js +531 -0
- package/dist/graph2d/curve.js.map +1 -0
- package/dist/graph2d/graph2d.d.ts +164 -0
- package/dist/graph2d/graph2d.d.ts.map +1 -0
- package/dist/graph2d/graph2d.js +404 -0
- package/dist/graph2d/graph2d.js.map +1 -0
- package/dist/graph2d/index.d.ts +32 -0
- package/dist/graph2d/index.d.ts.map +1 -0
- package/dist/graph2d/index.js +32 -0
- package/dist/graph2d/index.js.map +1 -0
- package/dist/graph2d/plane-fill.d.ts +110 -0
- package/dist/graph2d/plane-fill.d.ts.map +1 -0
- package/dist/graph2d/plane-fill.js +248 -0
- package/dist/graph2d/plane-fill.js.map +1 -0
- package/dist/graph2d/plane.d.ts +179 -0
- package/dist/graph2d/plane.d.ts.map +1 -0
- package/dist/graph2d/plane.js +359 -0
- package/dist/graph2d/plane.js.map +1 -0
- package/dist/graph2d/shared.d.ts +40 -0
- package/dist/graph2d/shared.d.ts.map +1 -0
- package/dist/graph2d/shared.js +70 -0
- package/dist/graph2d/shared.js.map +1 -0
- package/dist/graph3d/expression.d.ts +30 -0
- package/dist/graph3d/expression.d.ts.map +1 -0
- package/dist/graph3d/expression.js +35 -0
- package/dist/graph3d/expression.js.map +1 -0
- package/dist/graph3d/graph3d.d.ts +186 -0
- package/dist/graph3d/graph3d.d.ts.map +1 -0
- package/dist/graph3d/graph3d.js +404 -0
- package/dist/graph3d/graph3d.js.map +1 -0
- package/dist/graph3d/index.d.ts +22 -0
- package/dist/graph3d/index.d.ts.map +1 -0
- package/dist/graph3d/index.js +22 -0
- package/dist/graph3d/index.js.map +1 -0
- package/dist/graph3d/shared.d.ts +61 -0
- package/dist/graph3d/shared.d.ts.map +1 -0
- package/dist/graph3d/shared.js +101 -0
- package/dist/graph3d/shared.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/equations.d.ts +56 -0
- package/dist/kit/equations.d.ts.map +1 -0
- package/dist/kit/equations.js +63 -0
- package/dist/kit/equations.js.map +1 -0
- package/dist/kit/expression.d.ts +60 -0
- package/dist/kit/expression.d.ts.map +1 -0
- package/dist/kit/expression.js +268 -0
- package/dist/kit/expression.js.map +1 -0
- package/dist/kit/index.d.ts +15 -0
- package/dist/kit/index.d.ts.map +1 -0
- package/dist/kit/index.js +13 -0
- package/dist/kit/index.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/package.json +68 -3
- package/registry.json +34 -0
- package/src/engine.ts +13 -0
- package/src/graph2d/curve-cache.ts +103 -0
- package/src/graph2d/curve-error.ts +19 -0
- package/src/graph2d/curve.ts +657 -0
- package/src/graph2d/graph2d.ts +586 -0
- package/src/graph2d/index.ts +31 -0
- package/src/graph2d/plane-fill.ts +308 -0
- package/src/graph2d/plane.ts +457 -0
- package/src/graph2d/shared.ts +103 -0
- package/src/graph3d/expression.ts +51 -0
- package/src/graph3d/graph3d.ts +614 -0
- package/src/graph3d/index.ts +21 -0
- package/src/graph3d/shared.ts +143 -0
- package/src/index.ts +3 -0
- package/src/kit/equations.ts +102 -0
- package/src/kit/expression.ts +323 -0
- package/src/kit/index.ts +29 -0
- package/src/nodes.ts +19 -0
- package/README.md +0 -4
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ruled plane as a **paint** rather than as geometry.
|
|
3
|
+
*
|
|
4
|
+
* The grid used to be one filled rect per line — up to four hundred per axis per
|
|
5
|
+
* order, chained into one `Graphics` so at least the *draws* stayed constant.
|
|
6
|
+
* That is the arrangement `ChartCanvas` makes, and for a chart it is right: a
|
|
7
|
+
* chart's axes are ruled once and then sit still. A grapher's are not. Every
|
|
8
|
+
* rect moves on every frame of a pan, so the renderer's shape cache never hits;
|
|
9
|
+
* worse, the rect *count* changes as the ladder steps, and the cache is keyed by
|
|
10
|
+
* shape index, so a grid that gained a line shifted the key of every curve drawn
|
|
11
|
+
* after it and threw those away too.
|
|
12
|
+
*
|
|
13
|
+
* A shader has none of that. The plane is one shape whatever the zoom, so the
|
|
14
|
+
* curves' cache keys are stable, the wasm path behind it is built once, and the
|
|
15
|
+
* cost of ruling the plane stops depending on how much of it is ruled — which is
|
|
16
|
+
* the whole reason the old {@link MAX_TICKS} cap existed.
|
|
17
|
+
*
|
|
18
|
+
* ## Why not the built-in `grid` fill
|
|
19
|
+
*
|
|
20
|
+
* `Fills.grid` measures its pitch in *divisions of the shape* — deliberately, so
|
|
21
|
+
* a grid of eight stays a grid of eight when the shape resizes. A graph's pitch
|
|
22
|
+
* is decided by the tick ladder and is a different number of pixels on each
|
|
23
|
+
* axis, which one scalar `divisions` cannot say; and it carries one colour,
|
|
24
|
+
* where a graph has two orders in two colours plus its axes.
|
|
25
|
+
*
|
|
26
|
+
* ## Everything variable is a uniform
|
|
27
|
+
*
|
|
28
|
+
* `getOrCompileSkSL` caches by *source string* and never evicts, so a source
|
|
29
|
+
* built per frame is a new program per frame. {@link PLANE_SOURCE} is therefore a
|
|
30
|
+
* module constant and every switch — the two orders, the axes, the paper — is
|
|
31
|
+
* expressed by a uniform that can go to zero.
|
|
32
|
+
*
|
|
33
|
+
* ## The phase is the whole trick
|
|
34
|
+
*
|
|
35
|
+
* Uniforms reach the shader as **fp32**. Handing it "where x = 0 is" would be
|
|
36
|
+
* fine at the default zoom and 1e15 pixels at a deep one off the origin, which
|
|
37
|
+
* is not a number fp32 holds at all. So {@link planeUniforms} does the reduction
|
|
38
|
+
* in double precision on the way in: it splits the centre into a whole number of
|
|
39
|
+
* steps plus a remainder *before* anything is multiplied by the scale, and hands
|
|
40
|
+
* over a phase inside a single cell. Every number the shader sees is bounded by
|
|
41
|
+
* the node's own box.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import type { NormalizedColor } from "@motionscript/core"
|
|
45
|
+
|
|
46
|
+
import type { ResolvedPlane, TickLadder } from "./plane"
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The plane's paint program.
|
|
50
|
+
*
|
|
51
|
+
* Coverage-based rather than a hard test, and the ramp is one *device* pixel
|
|
52
|
+
* wide (`1 / u_scale`, the idiom every built-in pattern fill uses), so a ruling
|
|
53
|
+
* is a crisp hairline whether the camera is zoomed in on the node or the scene
|
|
54
|
+
* is being exported at four times scale.
|
|
55
|
+
*
|
|
56
|
+
* Uniforms are declared widest-first out of habit rather than necessity — a
|
|
57
|
+
* shader fill's uniforms are bound by *name* through reflection, unlike the
|
|
58
|
+
* built-in fills, whose flat positional array makes declaration order load
|
|
59
|
+
* bearing. `u_origin` and `u_scale` are supplied by the renderer.
|
|
60
|
+
*/
|
|
61
|
+
export const PLANE_SOURCE = `
|
|
62
|
+
uniform vec4 u_paper;
|
|
63
|
+
uniform vec4 u_minorColor;
|
|
64
|
+
uniform vec4 u_majorColor;
|
|
65
|
+
uniform vec4 u_axisColor;
|
|
66
|
+
uniform vec2 u_origin;
|
|
67
|
+
uniform vec2 u_pitch;
|
|
68
|
+
uniform vec2 u_phase;
|
|
69
|
+
uniform vec2 u_divisions;
|
|
70
|
+
uniform vec2 u_axisAt;
|
|
71
|
+
uniform float u_majorHalf;
|
|
72
|
+
uniform float u_minorHalf;
|
|
73
|
+
uniform float u_axisHalf;
|
|
74
|
+
uniform float u_scale;
|
|
75
|
+
|
|
76
|
+
// Coverage of a line \`d\` px away, of half-width \`w\`, ramped over \`feather\`.
|
|
77
|
+
//
|
|
78
|
+
// The zero test is not a shortcut. The ramp is centred on the line, so a line of
|
|
79
|
+
// no width still lands half a pixel of coverage on the pixels it runs through —
|
|
80
|
+
// an order you switched off, drawn faintly. Answering it here rather than in the
|
|
81
|
+
// ramp is what lets one compiled program serve showGrid, showMinorGrid, showAxes
|
|
82
|
+
// and a grid width of zero.
|
|
83
|
+
float ink(float d, float w, float feather) {
|
|
84
|
+
if (w <= 0.0) return 0.0;
|
|
85
|
+
return clamp((w - d) / feather + 0.5, 0.0, 1.0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Distance from \`t\` to the nearest ruling. SkSL's mod is floor-based, so this
|
|
89
|
+
// is right on either side of the phase without a sign test.
|
|
90
|
+
float toRuling(float t, float pitch, float phase) {
|
|
91
|
+
float m = mod(t - phase, pitch);
|
|
92
|
+
return min(m, pitch - m);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A straight colour under a coverage, premultiplied.
|
|
96
|
+
vec4 lay(vec4 color, float cover) {
|
|
97
|
+
float a = color.a * cover;
|
|
98
|
+
return vec4(color.rgb * a, a);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// \`src\` over \`dst\`, both premultiplied.
|
|
102
|
+
vec4 stack(vec4 src, vec4 dst) {
|
|
103
|
+
return src + dst * (1.0 - src.a);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
vec4 main(vec2 fragCoord) {
|
|
107
|
+
vec2 p = fragCoord - u_origin;
|
|
108
|
+
float feather = 1.0 / max(u_scale, 0.0001);
|
|
109
|
+
|
|
110
|
+
// Floored well above zero: a mod by zero paints garbage rather than nothing.
|
|
111
|
+
vec2 major = max(u_pitch, vec2(0.01));
|
|
112
|
+
vec2 minor = major / max(u_divisions, vec2(1.0));
|
|
113
|
+
|
|
114
|
+
vec2 dMajor = vec2(toRuling(p.x, major.x, u_phase.x),
|
|
115
|
+
toRuling(p.y, major.y, u_phase.y));
|
|
116
|
+
vec2 dMinor = vec2(toRuling(p.x, minor.x, u_phase.x),
|
|
117
|
+
toRuling(p.y, minor.y, u_phase.y));
|
|
118
|
+
|
|
119
|
+
float majorCover = max(ink(dMajor.x, u_majorHalf, feather),
|
|
120
|
+
ink(dMajor.y, u_majorHalf, feather));
|
|
121
|
+
|
|
122
|
+
// Every numbered ruling is also a faint one — the minor pitch divides the
|
|
123
|
+
// major exactly — so the faint order is cut away inside the numbered one's
|
|
124
|
+
// whole footprint, its ramp included. Drawing both would leave a hairline of
|
|
125
|
+
// the faint colour either side of every numbered line, where the numbered
|
|
126
|
+
// one's own ramp has not yet reached full coverage. The old ladder dropped
|
|
127
|
+
// coincident minors from a list; a shader has no list, so it subtracts.
|
|
128
|
+
float minorCover = max(
|
|
129
|
+
ink(dMinor.x, u_minorHalf, feather)
|
|
130
|
+
* (1.0 - ink(dMajor.x, u_majorHalf + feather, feather)),
|
|
131
|
+
ink(dMinor.y, u_minorHalf, feather)
|
|
132
|
+
* (1.0 - ink(dMajor.y, u_majorHalf + feather, feather)));
|
|
133
|
+
|
|
134
|
+
float axisCover = max(ink(abs(p.x - u_axisAt.x), u_axisHalf, feather),
|
|
135
|
+
ink(abs(p.y - u_axisAt.y), u_axisHalf, feather));
|
|
136
|
+
|
|
137
|
+
vec4 c = lay(u_paper, 1.0);
|
|
138
|
+
c = stack(lay(u_minorColor, minorCover), c);
|
|
139
|
+
c = stack(lay(u_majorColor, majorCover), c);
|
|
140
|
+
c = stack(lay(u_axisColor, axisCover), c);
|
|
141
|
+
return c;
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
|
|
145
|
+
/** What {@link planeUniforms} needs to know about how the plane is styled. */
|
|
146
|
+
export interface PlaneStyle {
|
|
147
|
+
/** The paper's colour. */
|
|
148
|
+
paper: NormalizedColor
|
|
149
|
+
/** How opaque the paper is, 0–1. At 0 the graph draws over what is behind it. */
|
|
150
|
+
paperOpacity: number
|
|
151
|
+
showGrid: boolean
|
|
152
|
+
showMinorGrid: boolean
|
|
153
|
+
showAxes: boolean
|
|
154
|
+
colorGrid: NormalizedColor
|
|
155
|
+
colorMinorGrid: NormalizedColor
|
|
156
|
+
colorAxis: NormalizedColor
|
|
157
|
+
/** Width of a numbered ruling. The faint ones are drawn at two thirds of it. */
|
|
158
|
+
lineWidth: number
|
|
159
|
+
/** Width of the two lines through the origin. */
|
|
160
|
+
axisWidth: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The uniform record {@link PLANE_SOURCE} is fed.
|
|
165
|
+
*
|
|
166
|
+
* A type alias rather than an interface so it satisfies the renderer's
|
|
167
|
+
* `Record<string, number | number[]>` without a cast — interfaces have no
|
|
168
|
+
* implicit index signature.
|
|
169
|
+
*/
|
|
170
|
+
export type PlaneUniforms = {
|
|
171
|
+
u_paper: number[]
|
|
172
|
+
u_minorColor: number[]
|
|
173
|
+
u_majorColor: number[]
|
|
174
|
+
u_axisColor: number[]
|
|
175
|
+
u_pitch: number[]
|
|
176
|
+
u_phase: number[]
|
|
177
|
+
u_divisions: number[]
|
|
178
|
+
u_axisAt: number[]
|
|
179
|
+
u_majorHalf: number
|
|
180
|
+
u_minorHalf: number
|
|
181
|
+
u_axisHalf: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Where an axis is parked when it is not on screen, in box pixels.
|
|
186
|
+
*
|
|
187
|
+
* Finite on purpose: an infinity here would reach the coverage ramp as a `NaN`,
|
|
188
|
+
* and a `NaN` compares false against every clamp, which paints the axis colour
|
|
189
|
+
* over the entire plane rather than nowhere on it.
|
|
190
|
+
*/
|
|
191
|
+
const AXIS_OFF = -1e7
|
|
192
|
+
|
|
193
|
+
/** Two thirds the width of a numbered ruling — the faint order's weight. */
|
|
194
|
+
const MINOR_WEIGHT = 2 / 3
|
|
195
|
+
|
|
196
|
+
/** Past this an integer index is no longer exact, so the reduction is meaningless. */
|
|
197
|
+
const MAX_INDEX = Number.MAX_SAFE_INTEGER
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether the plane has anything to paint at all — no paper, no rulings and no
|
|
201
|
+
* axes is a draw the node can skip outright.
|
|
202
|
+
*/
|
|
203
|
+
export function planeVisible(style: PlaneStyle): boolean {
|
|
204
|
+
if (style.paper[3] * clamp01(style.paperOpacity) > 0.001) return true
|
|
205
|
+
if (style.showGrid && style.lineWidth > 0) return true
|
|
206
|
+
return style.showAxes && style.axisWidth > 0
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The uniforms that put {@link PLANE_SOURCE} over a resolved plane.
|
|
211
|
+
*
|
|
212
|
+
* Everything handed over is bounded by the node's box — a pitch of tens of
|
|
213
|
+
* pixels, a phase inside one cell, an axis position inside the frame or parked
|
|
214
|
+
* at {@link AXIS_OFF}. That is deliberate and it is what makes the deep zoom
|
|
215
|
+
* work: see the module note.
|
|
216
|
+
*/
|
|
217
|
+
export function planeUniforms(
|
|
218
|
+
plane: ResolvedPlane,
|
|
219
|
+
style: PlaneStyle,
|
|
220
|
+
xTicks: TickLadder,
|
|
221
|
+
yTicks: TickLadder
|
|
222
|
+
): PlaneUniforms {
|
|
223
|
+
const ruled = style.showGrid && style.lineWidth > 0
|
|
224
|
+
const majorHalf = ruled ? style.lineWidth / 2 : 0
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
u_paper: withAlpha(style.paper, clamp01(style.paperOpacity)),
|
|
228
|
+
u_minorColor: rgba(style.colorMinorGrid),
|
|
229
|
+
u_majorColor: rgba(style.colorGrid),
|
|
230
|
+
u_axisColor: rgba(style.colorAxis),
|
|
231
|
+
u_pitch: [
|
|
232
|
+
pitchOf(xTicks.step, plane.scaleX),
|
|
233
|
+
pitchOf(yTicks.step, plane.scaleY),
|
|
234
|
+
],
|
|
235
|
+
// The shader's y runs down the box and the graph's runs up it, which is the
|
|
236
|
+
// whole of the difference between these two: one signed scale each.
|
|
237
|
+
u_phase: [
|
|
238
|
+
phaseOf(plane.centerX, xTicks.step, plane.width / 2, -plane.scaleX),
|
|
239
|
+
phaseOf(plane.centerY, yTicks.step, plane.height / 2, plane.scaleY),
|
|
240
|
+
],
|
|
241
|
+
u_divisions: [
|
|
242
|
+
Math.max(1, Math.round(xTicks.divisions)),
|
|
243
|
+
Math.max(1, Math.round(yTicks.divisions)),
|
|
244
|
+
],
|
|
245
|
+
u_axisAt: [
|
|
246
|
+
// Only ever evaluated when the origin is inside the range, which bounds
|
|
247
|
+
// `centre × scale` by half the box — the same reduction `phaseOf` makes,
|
|
248
|
+
// for free.
|
|
249
|
+
style.showAxes && plane.xMin <= 0 && 0 <= plane.xMax
|
|
250
|
+
? plane.width / 2 - plane.centerX * plane.scaleX
|
|
251
|
+
: AXIS_OFF,
|
|
252
|
+
style.showAxes && plane.yMin <= 0 && 0 <= plane.yMax
|
|
253
|
+
? plane.height / 2 + plane.centerY * plane.scaleY
|
|
254
|
+
: AXIS_OFF,
|
|
255
|
+
],
|
|
256
|
+
u_majorHalf: majorHalf,
|
|
257
|
+
u_minorHalf: ruled && style.showMinorGrid ? majorHalf * MINOR_WEIGHT : 0,
|
|
258
|
+
u_axisHalf: style.showAxes ? Math.max(0, style.axisWidth) / 2 : 0,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A ladder step in pixels, floored where the shader floors it. */
|
|
263
|
+
function pitchOf(step: number, scale: number): number {
|
|
264
|
+
const pitch = Math.abs(step * scale)
|
|
265
|
+
return Number.isFinite(pitch) && pitch > 0.01 ? pitch : 0.01
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Where the first ruling sits inside the box, in pixels from its leading edge —
|
|
270
|
+
* a number in `[0, pitch)`.
|
|
271
|
+
*
|
|
272
|
+
* The centre is split into a whole number of steps plus a remainder *before* it
|
|
273
|
+
* is ever multiplied by the scale, so the only quantity that ever carries the
|
|
274
|
+
* centre's own magnitude is an integer count that then cancels. Subtracting a
|
|
275
|
+
* tick value from the centre directly is exact at a span of 20 and has no
|
|
276
|
+
* significant digits left at 1e-9 around x = 1e6 — where the lines would still
|
|
277
|
+
* be drawn, just not evenly spaced.
|
|
278
|
+
*
|
|
279
|
+
* `signedScale` is negative for x and positive for y; see {@link planeUniforms}.
|
|
280
|
+
*/
|
|
281
|
+
function phaseOf(
|
|
282
|
+
center: number,
|
|
283
|
+
step: number,
|
|
284
|
+
halfBox: number,
|
|
285
|
+
signedScale: number
|
|
286
|
+
): number {
|
|
287
|
+
const pitch = pitchOf(step, signedScale)
|
|
288
|
+
const index = Math.round(center / step)
|
|
289
|
+
if (!Number.isFinite(index) || Math.abs(index) > MAX_INDEX) return 0
|
|
290
|
+
|
|
291
|
+
const remainder = center - index * step
|
|
292
|
+
const anchor = halfBox + remainder * signedScale
|
|
293
|
+
if (!Number.isFinite(anchor)) return 0
|
|
294
|
+
return anchor - Math.floor(anchor / pitch) * pitch
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function rgba(color: NormalizedColor): number[] {
|
|
298
|
+
return [color[0], color[1], color[2], color[3]]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function withAlpha(color: NormalizedColor, opacity: number): number[] {
|
|
302
|
+
return [color[0], color[1], color[2], color[3] * opacity]
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function clamp01(value: number): number {
|
|
306
|
+
if (!Number.isFinite(value)) return 1
|
|
307
|
+
return value < 0 ? 0 : value > 1 ? 1 : value
|
|
308
|
+
}
|