trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
146 lines (144 loc) • 3.94 kB
JavaScript
// src/timeline/core/index.ts
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function lowerBound(state) {
return state.range?.start ?? 0;
}
function upperBound(state) {
return state.range?.end ?? state.duration;
}
function createTimelineCore(config = {}) {
const base = {
duration: config.duration ?? 0,
position: 0,
rate: config.rate ?? 0,
loop: config.loop ?? true,
range: config.range ?? null,
marks: config.marks ?? [],
selectedMarkId: null,
step: config.step ?? 1 / 60
};
const startPosition = clamp(
config.position ?? 0,
lowerBound(base),
Math.max(upperBound(base), lowerBound(base))
);
let state = deriveState({ ...base, position: startPosition });
const subscribers = /* @__PURE__ */ new Set();
const notify = () => subscribers.forEach((fn) => fn());
function deriveState(partial) {
const lo = lowerBound(partial);
const hi = Math.max(upperBound(partial), lo);
const position = clamp(partial.position, lo, hi);
const selectedMark = partial.marks.find((m) => m.id === partial.selectedMarkId) ?? null;
return {
...partial,
position,
playing: partial.rate !== 0,
progress: partial.duration > 0 ? position / partial.duration : 0,
atStart: position <= lo,
atEnd: position >= hi,
selectedMark
};
}
const actions = {
play: () => {
state = deriveState({ ...state, rate: state.rate === 0 ? 1 : state.rate });
notify();
},
pause: () => {
if (state.rate === 0) return;
state = deriveState({ ...state, rate: 0 });
notify();
},
togglePlay: () => {
state = deriveState({ ...state, rate: state.rate === 0 ? 1 : 0 });
notify();
},
setRate: (rate) => {
if (rate === state.rate) return;
state = deriveState({ ...state, rate });
notify();
},
seek: (position) => {
state = deriveState({ ...state, position });
notify();
},
step: (deltaSeconds) => {
if (state.rate === 0) return;
const lo = lowerBound(state);
const hi = upperBound(state);
const raw = state.position + deltaSeconds * state.rate;
let next = raw;
let rate = state.rate;
if (next > hi) {
if (state.loop && hi > lo) {
next = lo + (next - hi) % Math.max(hi - lo, 1);
} else {
next = hi;
rate = 0;
}
} else if (next < lo) {
if (state.loop && hi > lo) {
next = hi - (lo - next) % Math.max(hi - lo, 1);
} else {
next = lo;
rate = 0;
}
}
state = deriveState({ ...state, position: next, rate });
notify();
},
setDuration: (duration) => {
const safe = Math.max(0, duration);
if (safe === state.duration) return;
state = deriveState({ ...state, duration: safe });
notify();
},
setLoop: (loop) => {
if (loop === state.loop) return;
state = deriveState({ ...state, loop });
notify();
},
setRange: (start, end) => {
if (end <= start) {
actions.clearRange();
return;
}
state = deriveState({ ...state, range: { start, end } });
notify();
},
clearRange: () => {
if (!state.range) return;
state = deriveState({ ...state, range: null });
notify();
},
setMarks: (marks) => {
state = deriveState({ ...state, marks });
notify();
},
selectMark: (id) => {
state = deriveState({ ...state, selectedMarkId: id });
notify();
},
setStep: (step) => {
state = deriveState({ ...state, step });
notify();
}
};
const core = {
get state() {
return state;
},
actions,
subscribe: (listener) => {
subscribers.add(listener);
return () => subscribers.delete(listener);
}
};
return core;
}
export {
createTimelineCore
};