@czap/quantizer 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/animated-quantizer.d.ts +108 -0
- package/dist/animated-quantizer.d.ts.map +1 -0
- package/dist/animated-quantizer.js +196 -0
- package/dist/animated-quantizer.js.map +1 -0
- package/dist/evaluate.d.ts +79 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +128 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/memo-cache.d.ts +35 -0
- package/dist/memo-cache.d.ts.map +1 -0
- package/dist/memo-cache.js +39 -0
- package/dist/memo-cache.js.map +1 -0
- package/dist/quantizer.d.ts +223 -0
- package/dist/quantizer.d.ts.map +1 -0
- package/dist/quantizer.js +260 -0
- package/dist/quantizer.js.map +1 -0
- package/dist/schemas.d.ts +44 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +46 -0
- package/dist/schemas.js.map +1 -0
- package/dist/testing.d.ts +15 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +15 -0
- package/dist/testing.js.map +1 -0
- package/dist/transition.d.ts +67 -0
- package/dist/transition.d.ts.map +1 -0
- package/dist/transition.js +49 -0
- package/dist/transition.js.map +1 -0
- package/package.json +58 -0
- package/src/animated-quantizer.ts +272 -0
- package/src/evaluate.ts +160 -0
- package/src/index.ts +26 -0
- package/src/memo-cache.ts +58 -0
- package/src/quantizer.ts +503 -0
- package/src/schemas.ts +50 -0
- package/src/testing.ts +15 -0
- package/src/transition.ts +97 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Eassa Ayoub <eassa@heyoub.dev>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @czap/quantizer
|
|
2
|
+
|
|
3
|
+
Q.from() builder, boundary evaluation, animated transitions, MotionTier gating.
|
|
4
|
+
|
|
5
|
+
## Docs
|
|
6
|
+
|
|
7
|
+
- [Naming & vocabulary](../../docs/GLOSSARY.md) — LiteShip, CZAP, `@czap/*`
|
|
8
|
+
|
|
9
|
+
- [API reference](https://github.com/heyoub/LiteShip/tree/main/docs/api/quantizer/) — generated from source TSDoc
|
|
10
|
+
- [Architecture index](https://github.com/heyoub/LiteShip/blob/main/docs/ARCHITECTURE.md)
|
|
11
|
+
- [ADRs](https://github.com/heyoub/LiteShip/tree/main/docs/adr/)
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @czap/quantizer
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Part of [LiteShip](https://github.com/heyoub/LiteShip#readme)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimatedQuantizer -- wraps a Quantizer with Transitions.
|
|
3
|
+
* On boundary crossing, interpolates between old and new output values
|
|
4
|
+
* over the configured transition duration/easing.
|
|
5
|
+
*/
|
|
6
|
+
import type { Scope } from 'effect';
|
|
7
|
+
import { Effect, Stream } from 'effect';
|
|
8
|
+
import type { Boundary, StateUnion, Quantizer } from '@czap/core';
|
|
9
|
+
import type { Transition, TransitionMap } from './transition.js';
|
|
10
|
+
/**
|
|
11
|
+
* Quantizer augmented with transition-aware output interpolation.
|
|
12
|
+
*
|
|
13
|
+
* The `interpolated` stream emits a frame on each animation tick containing
|
|
14
|
+
* the target state, normalized progress (0-1), and the current lerped
|
|
15
|
+
* output record. Non-numeric values snap at the 50% mark.
|
|
16
|
+
*/
|
|
17
|
+
export interface AnimatedQuantizerShape<B extends Boundary.Shape> extends Quantizer<B> {
|
|
18
|
+
/** Resolver that maps `from -> to` crossings to {@link TransitionConfig}. */
|
|
19
|
+
readonly transition: Transition<B>;
|
|
20
|
+
/** Stream of interpolated animation frames during crossings. */
|
|
21
|
+
readonly interpolated: Stream.Stream<{
|
|
22
|
+
/** Target state of the in-flight transition. */
|
|
23
|
+
readonly state: StateUnion<B>;
|
|
24
|
+
/** Progress in `[0, 1]`, where `1` means the animation has landed. */
|
|
25
|
+
readonly progress: number;
|
|
26
|
+
/** Interpolated output record for the current frame. */
|
|
27
|
+
readonly outputs: Record<string, number | string>;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create an animated quantizer that interpolates outputs during transitions.
|
|
32
|
+
*
|
|
33
|
+
* Wraps an existing {@link Quantizer} and applies easing/duration-based
|
|
34
|
+
* interpolation between old and new output values when a boundary crossing
|
|
35
|
+
* occurs. Produces an `interpolated` stream of frames with progress and
|
|
36
|
+
* lerped numeric outputs at ~60fps.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { Boundary } from '@czap/core';
|
|
41
|
+
* import { Q, AnimatedQuantizer } from '@czap/quantizer';
|
|
42
|
+
* import { Effect, Stream } from 'effect';
|
|
43
|
+
*
|
|
44
|
+
* const boundary = Boundary.make({
|
|
45
|
+
* input: 'scroll', states: ['top', 'bottom'] as const,
|
|
46
|
+
* thresholds: [0, 500],
|
|
47
|
+
* });
|
|
48
|
+
* const config = Q.from(boundary).outputs({
|
|
49
|
+
* css: { top: { opacity: '1' }, bottom: { opacity: '0.5' } },
|
|
50
|
+
* });
|
|
51
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
52
|
+
* const live = yield* config.create();
|
|
53
|
+
* const animated = yield* AnimatedQuantizer.make(
|
|
54
|
+
* live,
|
|
55
|
+
* { '*->*': { duration: 300 } },
|
|
56
|
+
* { top: { opacity: 1 }, bottom: { opacity: 0.5 } },
|
|
57
|
+
* );
|
|
58
|
+
* live.evaluate(600); // triggers interpolation
|
|
59
|
+
* return animated;
|
|
60
|
+
* }));
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @param quantizer - The base quantizer to wrap
|
|
64
|
+
* @param transitions - Map of state transition configs keyed by `from->to` pattern
|
|
65
|
+
* @param outputs - Per-state numeric output maps for interpolation
|
|
66
|
+
* @returns An Effect yielding an {@link AnimatedQuantizerShape} (scoped)
|
|
67
|
+
*/
|
|
68
|
+
declare function makeAnimatedQuantizer<B extends Boundary.Shape>(quantizer: Quantizer<B>, transitions: TransitionMap<StateUnion<B> & string>, outputs?: Record<string, Record<string, number | string>>): Effect.Effect<AnimatedQuantizerShape<B>, never, Scope.Scope>;
|
|
69
|
+
/**
|
|
70
|
+
* Animated quantizer namespace.
|
|
71
|
+
*
|
|
72
|
+
* Wraps a base quantizer with transition-aware interpolation. When a boundary
|
|
73
|
+
* crossing occurs, numeric output values are lerped over a configurable
|
|
74
|
+
* duration and easing curve. Non-numeric values snap at the 50% mark.
|
|
75
|
+
* The `interpolated` stream emits frames containing progress (0-1) and
|
|
76
|
+
* the current interpolated output record.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { Boundary } from '@czap/core';
|
|
81
|
+
* import { Q, AnimatedQuantizer } from '@czap/quantizer';
|
|
82
|
+
* import { Effect } from 'effect';
|
|
83
|
+
*
|
|
84
|
+
* const boundary = Boundary.make({
|
|
85
|
+
* input: 'scroll', states: ['top', 'bottom'] as const,
|
|
86
|
+
* thresholds: [0, 500],
|
|
87
|
+
* });
|
|
88
|
+
* const config = Q.from(boundary).outputs({});
|
|
89
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
90
|
+
* const live = yield* config.create();
|
|
91
|
+
* const animated = yield* AnimatedQuantizer.make(
|
|
92
|
+
* live,
|
|
93
|
+
* { '*->*': { duration: 200 } },
|
|
94
|
+
* );
|
|
95
|
+
* return animated.transition; // TransitionResolver
|
|
96
|
+
* }));
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare const AnimatedQuantizer: {
|
|
100
|
+
/** Wrap a quantizer with transition-aware output interpolation. */
|
|
101
|
+
readonly make: typeof makeAnimatedQuantizer;
|
|
102
|
+
};
|
|
103
|
+
export declare namespace AnimatedQuantizer {
|
|
104
|
+
/** Shape of an animated quantizer parameterized by boundary `B`. */
|
|
105
|
+
type Shape<B extends Boundary.Shape> = AnimatedQuantizerShape<B>;
|
|
106
|
+
}
|
|
107
|
+
export {};
|
|
108
|
+
//# sourceMappingURL=animated-quantizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animated-quantizer.d.ts","sourceRoot":"","sources":["../src/animated-quantizer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAgD,MAAM,QAAQ,CAAC;AACtF,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAoB,SAAS,EAAU,MAAM,YAAY,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAOjE;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,QAAQ,CAAC,KAAK,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IACpF,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACnC,gEAAgE;IAChE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC;QACnC,gDAAgD;QAChD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9B,sEAAsE;QACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,wDAAwD;QACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;KACnD,CAAC,CAAC;CACJ;AA6CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,iBAAS,qBAAqB,CAAC,CAAC,SAAS,QAAQ,CAAC,KAAK,EACrD,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,EACvB,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,EAClD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GACxD,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAyG9D;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,iBAAiB;IAC5B,mEAAmE;;CAE3D,CAAC;AAEX,MAAM,CAAC,OAAO,WAAW,iBAAiB,CAAC;IACzC,oEAAoE;IACpE,KAAY,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,KAAK,IAAI,sBAAsB,CAAC,CAAC,CAAC,CAAC;CACzE"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimatedQuantizer -- wraps a Quantizer with Transitions.
|
|
3
|
+
* On boundary crossing, interpolates between old and new output values
|
|
4
|
+
* over the configured transition duration/easing.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect, Stream, SubscriptionRef, Queue, Fiber, Ref, Duration } from 'effect';
|
|
7
|
+
import { Transition as TransitionFactory } from './transition.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Linear easing fallback
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const linearEasing = (t) => t;
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Interpolate numeric values between two output records
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function lerpOutputs(from, to, t) {
|
|
16
|
+
const result = {};
|
|
17
|
+
const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);
|
|
18
|
+
for (const key of allKeys) {
|
|
19
|
+
const a = from[key];
|
|
20
|
+
const b = to[key];
|
|
21
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
22
|
+
result[key] = a + (b - a) * t;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Non-numeric values snap to target at progress >= 0.5
|
|
26
|
+
result[key] = (t < 0.5 ? (a ?? b) : (b ?? a));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function nowMs() {
|
|
32
|
+
// performance.now() is standard in browsers and Node ≥ 16.
|
|
33
|
+
// Optional chaining guards against stripped worker/SSR environments.
|
|
34
|
+
if (typeof globalThis.performance?.now === 'function') {
|
|
35
|
+
return globalThis.performance.now();
|
|
36
|
+
}
|
|
37
|
+
return Date.now();
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Factory (internal impl)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Create an animated quantizer that interpolates outputs during transitions.
|
|
44
|
+
*
|
|
45
|
+
* Wraps an existing {@link Quantizer} and applies easing/duration-based
|
|
46
|
+
* interpolation between old and new output values when a boundary crossing
|
|
47
|
+
* occurs. Produces an `interpolated` stream of frames with progress and
|
|
48
|
+
* lerped numeric outputs at ~60fps.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { Boundary } from '@czap/core';
|
|
53
|
+
* import { Q, AnimatedQuantizer } from '@czap/quantizer';
|
|
54
|
+
* import { Effect, Stream } from 'effect';
|
|
55
|
+
*
|
|
56
|
+
* const boundary = Boundary.make({
|
|
57
|
+
* input: 'scroll', states: ['top', 'bottom'] as const,
|
|
58
|
+
* thresholds: [0, 500],
|
|
59
|
+
* });
|
|
60
|
+
* const config = Q.from(boundary).outputs({
|
|
61
|
+
* css: { top: { opacity: '1' }, bottom: { opacity: '0.5' } },
|
|
62
|
+
* });
|
|
63
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
64
|
+
* const live = yield* config.create();
|
|
65
|
+
* const animated = yield* AnimatedQuantizer.make(
|
|
66
|
+
* live,
|
|
67
|
+
* { '*->*': { duration: 300 } },
|
|
68
|
+
* { top: { opacity: 1 }, bottom: { opacity: 0.5 } },
|
|
69
|
+
* );
|
|
70
|
+
* live.evaluate(600); // triggers interpolation
|
|
71
|
+
* return animated;
|
|
72
|
+
* }));
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param quantizer - The base quantizer to wrap
|
|
76
|
+
* @param transitions - Map of state transition configs keyed by `from->to` pattern
|
|
77
|
+
* @param outputs - Per-state numeric output maps for interpolation
|
|
78
|
+
* @returns An Effect yielding an {@link AnimatedQuantizerShape} (scoped)
|
|
79
|
+
*/
|
|
80
|
+
function makeAnimatedQuantizer(quantizer, transitions, outputs) {
|
|
81
|
+
return Effect.gen(function* () {
|
|
82
|
+
const boundary = quantizer.boundary;
|
|
83
|
+
const transitionResolver = TransitionFactory.for(quantizer, transitions);
|
|
84
|
+
const initialState = yield* quantizer.state;
|
|
85
|
+
const stateRef = yield* SubscriptionRef.make(initialState);
|
|
86
|
+
const currentOutputsRef = yield* Ref.make(outputs?.[initialState] ?? {});
|
|
87
|
+
const currentFiberRef = yield* Ref.make(null);
|
|
88
|
+
const interpolatedStream = Stream.callback((queue) => Effect.gen(function* () {
|
|
89
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
90
|
+
const currentFiber = yield* Ref.get(currentFiberRef);
|
|
91
|
+
if (currentFiber !== null) {
|
|
92
|
+
yield* Fiber.interrupt(currentFiber);
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
yield* Stream.runForEach(quantizer.changes, (crossing) => Effect.gen(function* () {
|
|
96
|
+
const existingFiber = yield* Ref.get(currentFiberRef);
|
|
97
|
+
if (existingFiber !== null) {
|
|
98
|
+
yield* Fiber.interrupt(existingFiber);
|
|
99
|
+
}
|
|
100
|
+
// crossing.from/to are StateName<StateUnion<B> & string>, which is a branded
|
|
101
|
+
// subtype of StateUnion<B>; assignable directly without a cast.
|
|
102
|
+
const { from, to } = crossing;
|
|
103
|
+
const config = transitionResolver.getTransition(from, to);
|
|
104
|
+
const duration = config.duration;
|
|
105
|
+
const easing = config.easing ?? linearEasing;
|
|
106
|
+
const delay = config.delay ?? 0;
|
|
107
|
+
const fromOutputs = { ...(yield* Ref.get(currentOutputsRef)) };
|
|
108
|
+
const toOutputs = outputs?.[crossing.to] ?? {};
|
|
109
|
+
const animationLoop = Effect.gen(function* () {
|
|
110
|
+
if (delay > 0) {
|
|
111
|
+
yield* Effect.sleep(Duration.millis(delay));
|
|
112
|
+
}
|
|
113
|
+
if (duration <= 0) {
|
|
114
|
+
Queue.offerUnsafe(queue, { state: to, progress: 1, outputs: toOutputs });
|
|
115
|
+
yield* Ref.set(currentOutputsRef, toOutputs);
|
|
116
|
+
yield* SubscriptionRef.set(stateRef, to);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Time-sliced animation loop (~60fps via 16ms sleep)
|
|
120
|
+
const startTime = nowMs();
|
|
121
|
+
let progress = 0;
|
|
122
|
+
while (progress < 1) {
|
|
123
|
+
const elapsed = nowMs() - startTime;
|
|
124
|
+
progress = Math.min(elapsed / duration, 1);
|
|
125
|
+
const eased = easing(progress);
|
|
126
|
+
const interpolated = lerpOutputs(fromOutputs, toOutputs, eased);
|
|
127
|
+
yield* Ref.set(currentOutputsRef, interpolated);
|
|
128
|
+
Queue.offerUnsafe(queue, { state: to, progress, outputs: interpolated });
|
|
129
|
+
if (progress < 1) {
|
|
130
|
+
yield* Effect.sleep(Duration.millis(16));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
yield* Ref.set(currentOutputsRef, toOutputs);
|
|
134
|
+
yield* SubscriptionRef.set(stateRef, to);
|
|
135
|
+
});
|
|
136
|
+
const fiber = yield* Effect.forkChild(animationLoop);
|
|
137
|
+
yield* Ref.set(currentFiberRef, fiber);
|
|
138
|
+
}));
|
|
139
|
+
const finalFiber = yield* Ref.get(currentFiberRef);
|
|
140
|
+
const fibers = [finalFiber].filter((fiber) => fiber !== null);
|
|
141
|
+
yield* Effect.forEach(fibers, Fiber.join, { discard: true });
|
|
142
|
+
yield* Ref.set(currentFiberRef, null);
|
|
143
|
+
}));
|
|
144
|
+
const animatedQuantizer = {
|
|
145
|
+
_tag: 'Quantizer',
|
|
146
|
+
boundary,
|
|
147
|
+
transition: transitionResolver,
|
|
148
|
+
state: SubscriptionRef.get(stateRef),
|
|
149
|
+
stateSync: quantizer.stateSync ? () => quantizer.stateSync() : undefined,
|
|
150
|
+
changes: quantizer.changes,
|
|
151
|
+
evaluate(value) {
|
|
152
|
+
return quantizer.evaluate(value);
|
|
153
|
+
},
|
|
154
|
+
interpolated: interpolatedStream,
|
|
155
|
+
};
|
|
156
|
+
return animatedQuantizer;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// AnimatedQuantizer module object
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/**
|
|
163
|
+
* Animated quantizer namespace.
|
|
164
|
+
*
|
|
165
|
+
* Wraps a base quantizer with transition-aware interpolation. When a boundary
|
|
166
|
+
* crossing occurs, numeric output values are lerped over a configurable
|
|
167
|
+
* duration and easing curve. Non-numeric values snap at the 50% mark.
|
|
168
|
+
* The `interpolated` stream emits frames containing progress (0-1) and
|
|
169
|
+
* the current interpolated output record.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* import { Boundary } from '@czap/core';
|
|
174
|
+
* import { Q, AnimatedQuantizer } from '@czap/quantizer';
|
|
175
|
+
* import { Effect } from 'effect';
|
|
176
|
+
*
|
|
177
|
+
* const boundary = Boundary.make({
|
|
178
|
+
* input: 'scroll', states: ['top', 'bottom'] as const,
|
|
179
|
+
* thresholds: [0, 500],
|
|
180
|
+
* });
|
|
181
|
+
* const config = Q.from(boundary).outputs({});
|
|
182
|
+
* const program = Effect.scoped(Effect.gen(function* () {
|
|
183
|
+
* const live = yield* config.create();
|
|
184
|
+
* const animated = yield* AnimatedQuantizer.make(
|
|
185
|
+
* live,
|
|
186
|
+
* { '*->*': { duration: 200 } },
|
|
187
|
+
* );
|
|
188
|
+
* return animated.transition; // TransitionResolver
|
|
189
|
+
* }));
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export const AnimatedQuantizer = {
|
|
193
|
+
/** Wrap a quantizer with transition-aware output interpolation. */
|
|
194
|
+
make: makeAnimatedQuantizer,
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=animated-quantizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animated-quantizer.js","sourceRoot":"","sources":["../src/animated-quantizer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGtF,OAAO,EAAE,UAAU,IAAI,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AA2BlE,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,MAAM,YAAY,GAAc,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC;AAEjD,8EAA8E;AAC9E,wDAAwD;AACxD,8EAA8E;AAE9E,SAAS,WAAW,CAClB,IAAqC,EACrC,EAAmC,EACnC,CAAS;IAET,MAAM,MAAM,GAAoC,EAAE,CAAC;IACnD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACpE,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QAClB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YACnD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACN,uDAAuD;YACvD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAoB,CAAC;QACnE,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,KAAK;IACZ,2DAA2D;IAC3D,qEAAqE;IACrE,IAAI,OAAO,UAAU,CAAC,WAAW,EAAE,GAAG,KAAK,UAAU,EAAE,CAAC;QACtD,OAAO,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC;AAED,8EAA8E;AAC9E,0BAA0B;AAC1B,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,SAAS,qBAAqB,CAC5B,SAAuB,EACvB,WAAkD,EAClD,OAAyD;IAEzD,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzB,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;QACpC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,GAAG,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAEzE,MAAM,YAAY,GAAkB,KAAK,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC;QAC3D,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CAAgB,YAAY,CAAC,CAAC;QAQ1E,MAAM,iBAAiB,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAkC,OAAO,EAAE,CAAC,YAAsB,CAAC,IAAI,EAAE,CAAC,CAAC;QACpH,MAAM,eAAe,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAA2B,IAAI,CAAC,CAAC;QAExE,MAAM,kBAAkB,GAAqC,MAAM,CAAC,QAAQ,CAAoB,CAAC,KAAK,EAAE,EAAE,CACxG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAC9B,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBACrD,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;oBAC1B,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC,CAAC,CACH,CAAC;YAEF,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,QAAkD,EAAE,EAAE,CACjG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,MAAM,aAAa,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBACtD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;oBAC3B,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;gBACxC,CAAC;gBAED,6EAA6E;gBAC7E,gEAAgE;gBAChE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC;gBAC9B,MAAM,MAAM,GAAG,kBAAkB,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;gBACjC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,YAAY,CAAC;gBAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;gBAEhC,MAAM,WAAW,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC;gBAC/D,MAAM,SAAS,GAAoC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAY,CAAC,IAAI,EAAE,CAAC;gBAE1F,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;oBACxC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;wBACd,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;oBAC9C,CAAC;oBAED,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;wBAClB,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;wBACzE,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAC;wBAC7C,KAAK,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;wBACzC,OAAO;oBACT,CAAC;oBAED,qDAAqD;oBACrD,MAAM,SAAS,GAAG,KAAK,EAAE,CAAC;oBAC1B,IAAI,QAAQ,GAAG,CAAC,CAAC;oBACjB,OAAO,QAAQ,GAAG,CAAC,EAAE,CAAC;wBACpB,MAAM,OAAO,GAAG,KAAK,EAAE,GAAG,SAAS,CAAC;wBACpC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;wBAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;wBAC/B,MAAM,YAAY,GAAG,WAAW,CAAC,WAAW,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;wBAChE,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;wBAChD,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;wBAEzE,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;4BACjB,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;wBAC3C,CAAC;oBACH,CAAC;oBAED,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,SAAS,CAAC,CAAC;oBAC7C,KAAK,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;gBAC3C,CAAC,CAAC,CAAC;gBAEH,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;gBACrD,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;YACzC,CAAC,CAAC,CACH,CAAC;YAEF,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAA8B,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;YAC1F,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7D,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC,CAAC,CACH,CAAC;QAEF,MAAM,iBAAiB,GAA8B;YACnD,IAAI,EAAE,WAAW;YACjB,QAAQ;YACR,UAAU,EAAE,kBAAkB;YAC9B,KAAK,EAAE,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC;YACpC,SAAS,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,SAAU,EAAE,CAAC,CAAC,CAAC,SAAS;YACzE,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,QAAQ,CAAC,KAAa;gBACpB,OAAO,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACnC,CAAC;YACD,YAAY,EAAE,kBAAkB;SACjC,CAAC;QAEF,OAAO,iBAAiB,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,mEAAmE;IACnE,IAAI,EAAE,qBAAqB;CACnB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary search evaluation of a value against boundary thresholds.
|
|
3
|
+
* Supports hysteresis to prevent state jitter at threshold edges.
|
|
4
|
+
*/
|
|
5
|
+
import type { Boundary, StateUnion } from '@czap/core';
|
|
6
|
+
/**
|
|
7
|
+
* Result of quantizing a single numeric value against a boundary.
|
|
8
|
+
*
|
|
9
|
+
* `crossed` is true only when `previousState` was supplied and differs
|
|
10
|
+
* from the resolved state; it is the signal consumers use to emit
|
|
11
|
+
* transition events and route side effects.
|
|
12
|
+
*/
|
|
13
|
+
export interface EvaluateResult<S extends string = string> {
|
|
14
|
+
/** The resolved state literal. */
|
|
15
|
+
readonly state: S;
|
|
16
|
+
/** Index of `state` within the boundary's states tuple. */
|
|
17
|
+
readonly index: number;
|
|
18
|
+
/** The input value that was evaluated. */
|
|
19
|
+
readonly value: number;
|
|
20
|
+
/** Whether evaluation produced a change from `previousState`. */
|
|
21
|
+
readonly crossed: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find which state a value maps to via binary search over sorted thresholds.
|
|
25
|
+
* With hysteresis: if previousState is provided and the value is within the
|
|
26
|
+
* hysteresis dead zone of a threshold, transition is suppressed.
|
|
27
|
+
*
|
|
28
|
+
* BoundaryDef contract: `thresholds[i]` = lower bound of `states[i]`.
|
|
29
|
+
* Binary search finds the largest index `i` where `thresholds[i] <= value`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { Boundary } from '@czap/core';
|
|
34
|
+
* import { evaluate } from '@czap/quantizer';
|
|
35
|
+
*
|
|
36
|
+
* const boundary = Boundary.make({
|
|
37
|
+
* input: 'width', states: ['sm', 'md', 'lg'] as const,
|
|
38
|
+
* thresholds: [0, 640, 1024], hysteresis: 20,
|
|
39
|
+
* });
|
|
40
|
+
* const result = evaluate(boundary, 800);
|
|
41
|
+
* // result => { state: 'md', index: 1, value: 800, crossed: false }
|
|
42
|
+
*
|
|
43
|
+
* const cross = evaluate(boundary, 1100, 'md');
|
|
44
|
+
* // cross => { state: 'lg', index: 2, value: 1100, crossed: true }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param boundary - The boundary definition with states and thresholds
|
|
48
|
+
* @param value - The numeric value to evaluate
|
|
49
|
+
* @param previousState - Optional previous state for hysteresis and crossing detection
|
|
50
|
+
* @returns An {@link EvaluateResult} with the resolved state, index, and crossing flag
|
|
51
|
+
*/
|
|
52
|
+
export declare function evaluate<B extends Boundary.Shape>(boundary: B, value: number, previousState?: StateUnion<B>): EvaluateResult<StateUnion<B> & string>;
|
|
53
|
+
/**
|
|
54
|
+
* Boundary evaluation namespace.
|
|
55
|
+
*
|
|
56
|
+
* Provides `evaluate()` for mapping a numeric value to a discrete state
|
|
57
|
+
* via binary search over boundary thresholds with optional hysteresis
|
|
58
|
+
* to prevent jitter at threshold edges.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { Boundary } from '@czap/core';
|
|
63
|
+
* import { Evaluate } from '@czap/quantizer';
|
|
64
|
+
*
|
|
65
|
+
* const boundary = Boundary.make({
|
|
66
|
+
* input: 'width', states: ['sm', 'lg'] as const,
|
|
67
|
+
* thresholds: [0, 768], hysteresis: 10,
|
|
68
|
+
* });
|
|
69
|
+
* const r1 = Evaluate.evaluate(boundary, 500);
|
|
70
|
+
* // r1.state => 'sm', r1.crossed => false
|
|
71
|
+
*
|
|
72
|
+
* const r2 = Evaluate.evaluate(boundary, 900, 'sm');
|
|
73
|
+
* // r2.state => 'lg', r2.crossed => true
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare const Evaluate: {
|
|
77
|
+
readonly evaluate: typeof evaluate;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../src/evaluate.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IACvD,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,QAAQ,CAAC,KAAK,EAC/C,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAC5B,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CA4ExC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,QAAQ;;CAAwB,CAAC"}
|
package/dist/evaluate.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary search evaluation of a value against boundary thresholds.
|
|
3
|
+
* Supports hysteresis to prevent state jitter at threshold edges.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Find which state a value maps to via binary search over sorted thresholds.
|
|
7
|
+
* With hysteresis: if previousState is provided and the value is within the
|
|
8
|
+
* hysteresis dead zone of a threshold, transition is suppressed.
|
|
9
|
+
*
|
|
10
|
+
* BoundaryDef contract: `thresholds[i]` = lower bound of `states[i]`.
|
|
11
|
+
* Binary search finds the largest index `i` where `thresholds[i] <= value`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { Boundary } from '@czap/core';
|
|
16
|
+
* import { evaluate } from '@czap/quantizer';
|
|
17
|
+
*
|
|
18
|
+
* const boundary = Boundary.make({
|
|
19
|
+
* input: 'width', states: ['sm', 'md', 'lg'] as const,
|
|
20
|
+
* thresholds: [0, 640, 1024], hysteresis: 20,
|
|
21
|
+
* });
|
|
22
|
+
* const result = evaluate(boundary, 800);
|
|
23
|
+
* // result => { state: 'md', index: 1, value: 800, crossed: false }
|
|
24
|
+
*
|
|
25
|
+
* const cross = evaluate(boundary, 1100, 'md');
|
|
26
|
+
* // cross => { state: 'lg', index: 2, value: 1100, crossed: true }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @param boundary - The boundary definition with states and thresholds
|
|
30
|
+
* @param value - The numeric value to evaluate
|
|
31
|
+
* @param previousState - Optional previous state for hysteresis and crossing detection
|
|
32
|
+
* @returns An {@link EvaluateResult} with the resolved state, index, and crossing flag
|
|
33
|
+
*/
|
|
34
|
+
export function evaluate(boundary, value, previousState) {
|
|
35
|
+
const { thresholds, states, hysteresis } = boundary;
|
|
36
|
+
// Boundary.make guarantees states is non-empty; index access below yields StateUnion<B>.
|
|
37
|
+
// `& string` is structurally satisfied because every state literal is a string.
|
|
38
|
+
const stateAt = (index) => states[index];
|
|
39
|
+
if (thresholds.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
state: stateAt(0),
|
|
42
|
+
index: 0,
|
|
43
|
+
value,
|
|
44
|
+
crossed: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Binary search: find largest index i where thresholds[i] <= value
|
|
48
|
+
// This gives the state whose lower bound is satisfied.
|
|
49
|
+
let lo = 0;
|
|
50
|
+
let hi = thresholds.length - 1;
|
|
51
|
+
let rawIndex = 0; // default: first state (value below all thresholds)
|
|
52
|
+
while (lo <= hi) {
|
|
53
|
+
const mid = (lo + hi) >>> 1;
|
|
54
|
+
if (thresholds[mid] <= value) {
|
|
55
|
+
rawIndex = mid;
|
|
56
|
+
lo = mid + 1;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
hi = mid - 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const state = stateAt(rawIndex);
|
|
63
|
+
// Without hysteresis or no previous state, return raw result
|
|
64
|
+
if (!hysteresis || hysteresis <= 0 || previousState === undefined) {
|
|
65
|
+
const crossed = previousState !== undefined && previousState !== state;
|
|
66
|
+
return { state, index: rawIndex, value, crossed };
|
|
67
|
+
}
|
|
68
|
+
// Find previous state index
|
|
69
|
+
const prevIndex = states.indexOf(previousState);
|
|
70
|
+
if (prevIndex === -1) {
|
|
71
|
+
return { state, index: rawIndex, value, crossed: true };
|
|
72
|
+
}
|
|
73
|
+
// No crossing needed
|
|
74
|
+
if (rawIndex === prevIndex) {
|
|
75
|
+
return { state, index: rawIndex, value, crossed: false };
|
|
76
|
+
}
|
|
77
|
+
// Half-width hysteresis: dead zone of h/2 each side of threshold
|
|
78
|
+
const half = hysteresis / 2;
|
|
79
|
+
// Check ALL intermediate thresholds for dead zone suppression
|
|
80
|
+
if (rawIndex > prevIndex) {
|
|
81
|
+
// Crossing upward -- check thresholds from prevIndex+1 to rawIndex
|
|
82
|
+
for (let i = prevIndex + 1; i <= rawIndex; i++) {
|
|
83
|
+
const threshold = thresholds[i];
|
|
84
|
+
if (threshold !== undefined && value < threshold + half) {
|
|
85
|
+
// In dead zone -- settle at state just below this threshold
|
|
86
|
+
const settleIndex = i - 1;
|
|
87
|
+
return { state: stateAt(settleIndex), index: settleIndex, value, crossed: settleIndex !== prevIndex };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Crossing downward -- check thresholds from prevIndex down to rawIndex+1
|
|
93
|
+
for (let i = prevIndex; i > rawIndex; i--) {
|
|
94
|
+
const threshold = thresholds[i];
|
|
95
|
+
if (threshold !== undefined && value > threshold - half) {
|
|
96
|
+
// In dead zone -- settle at this state
|
|
97
|
+
return { state: stateAt(i), index: i, value, crossed: i !== prevIndex };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Cleared all dead zones -- full transition
|
|
102
|
+
return { state, index: rawIndex, value, crossed: true };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Boundary evaluation namespace.
|
|
106
|
+
*
|
|
107
|
+
* Provides `evaluate()` for mapping a numeric value to a discrete state
|
|
108
|
+
* via binary search over boundary thresholds with optional hysteresis
|
|
109
|
+
* to prevent jitter at threshold edges.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* import { Boundary } from '@czap/core';
|
|
114
|
+
* import { Evaluate } from '@czap/quantizer';
|
|
115
|
+
*
|
|
116
|
+
* const boundary = Boundary.make({
|
|
117
|
+
* input: 'width', states: ['sm', 'lg'] as const,
|
|
118
|
+
* thresholds: [0, 768], hysteresis: 10,
|
|
119
|
+
* });
|
|
120
|
+
* const r1 = Evaluate.evaluate(boundary, 500);
|
|
121
|
+
* // r1.state => 'sm', r1.crossed => false
|
|
122
|
+
*
|
|
123
|
+
* const r2 = Evaluate.evaluate(boundary, 900, 'sm');
|
|
124
|
+
* // r2.state => 'lg', r2.crossed => true
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export const Evaluate = { evaluate };
|
|
128
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../src/evaluate.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@czap/quantizer` — **LiteShip** quantizer: **rigged** boundary evaluation,
|
|
3
|
+
* live state, animated transitions between bearings, and motion-tier gating on
|
|
4
|
+
* the working line.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { evaluate, Evaluate } from './evaluate.js';
|
|
9
|
+
export type { EvaluateResult } from './evaluate.js';
|
|
10
|
+
export { Q } from './quantizer.js';
|
|
11
|
+
export type { OutputTarget, QuantizerOutputs, QuantizerConfig, LiveQuantizer, QuantizerBuilder } from './quantizer.js';
|
|
12
|
+
export { Transition } from './transition.js';
|
|
13
|
+
export type { TransitionConfig, TransitionMap, Transition as TransitionType } from './transition.js';
|
|
14
|
+
export { AnimatedQuantizer } from './animated-quantizer.js';
|
|
15
|
+
export type { AnimatedQuantizerShape } from './animated-quantizer.js';
|
|
16
|
+
export { TransitionConfigSchema, TransitionMapSchema, OutputTargetSchema, QuantizerOutputsSchema } from './schemas.js';
|
|
17
|
+
export type { MotionTier, SpringConfig, QuantizerFromOptions } from './quantizer.js';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EAAE,CAAC,EAAE,MAAM,gBAAgB,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErG,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEtE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAEvH,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@czap/quantizer` — **LiteShip** quantizer: **rigged** boundary evaluation,
|
|
3
|
+
* live state, animated transitions between bearings, and motion-tier gating on
|
|
4
|
+
* the working line.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { evaluate, Evaluate } from './evaluate.js';
|
|
9
|
+
export { Q } from './quantizer.js';
|
|
10
|
+
export { Transition } from './transition.js';
|
|
11
|
+
export { AnimatedQuantizer } from './animated-quantizer.js';
|
|
12
|
+
export { TransitionConfigSchema, TransitionMapSchema, OutputTargetSchema, QuantizerOutputsSchema } from './schemas.js';
|
|
13
|
+
// `MemoCache` and `TIER_TARGETS` ship via `@czap/quantizer/testing` —
|
|
14
|
+
// implementation primitives that power the public `Q.from()` builder
|
|
15
|
+
// internally but are not consumer-facing API.
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,EAAE,CAAC,EAAE,MAAM,gBAAgB,CAAC;AAGnC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAGvH,sEAAsE;AACtE,qEAAqE;AACrE,8CAA8C"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoCache -- content-address memoization layer.
|
|
3
|
+
*
|
|
4
|
+
* Boundaries and quantizer configs are already content-addressed via FNV-1a.
|
|
5
|
+
* This cache ensures identical configs never recompute. Content-addressed keys
|
|
6
|
+
* mean the cache is auto-invalidating: change definition → hash changes → miss.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import type { ContentAddress } from '@czap/core';
|
|
11
|
+
interface MemoCacheShape<V> {
|
|
12
|
+
get(key: ContentAddress): V | undefined;
|
|
13
|
+
set(key: ContentAddress, value: V): void;
|
|
14
|
+
has(key: ContentAddress): boolean;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
}
|
|
17
|
+
declare function _make<V>(): MemoCacheShape<V>;
|
|
18
|
+
/**
|
|
19
|
+
* Content-address memoization cache.
|
|
20
|
+
*
|
|
21
|
+
* Keys are {@link ContentAddress} values, so the cache is auto-invalidating:
|
|
22
|
+
* any change to an upstream definition produces a new hash and a guaranteed
|
|
23
|
+
* miss. Backed by an unbounded {@link Map}; callers are responsible for
|
|
24
|
+
* lifetime and eviction if needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MemoCache: {
|
|
27
|
+
/** Construct a fresh cache with value type `V`. */
|
|
28
|
+
make: typeof _make;
|
|
29
|
+
};
|
|
30
|
+
export declare namespace MemoCache {
|
|
31
|
+
/** Structural shape of a {@link MemoCache} with value type `V`. */
|
|
32
|
+
type Shape<V> = MemoCacheShape<V>;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=memo-cache.d.ts.map
|