add Pattern Context.

This commit is contained in:
Vixalie 2025-03-06 14:29:32 +08:00
parent 19198d57d9
commit 4a2f599bff

157
src/context/Patterns.tsx Normal file
View File

@ -0,0 +1,157 @@
import { invoke } from '@tauri-apps/api/core';
import dayjs from 'dayjs';
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { get, reduce } from 'lodash-es';
import { v4 } from 'uuid';
export enum FrequencyShifting {
/**
* Change frequency undergoes a linear transformation from previous pulse to current one.
*/
Linear,
/**
* Change frequency undergoes a quadratic transformation from previous pulse to current one.
*/
Quadratic,
/**
* Change frequency undergoes a cubic transformation from previous pulse to current one.
*/
Cubic,
/**
* Change frequency with quick fade in and fade out.
*/
Ease,
/**
* Change frequency with spiking within range from previous pulse and current one.
*/
Pulsating,
/**
* Based on frequency of previous pulse, take the twice of frequency of current pulse as the peak frequency. Follow the maximum frequency limitation.
*/
Spiking,
/**
* Randomize frequency within range from previous frequency and current one.
*/
Randomize,
/**
* Randomize frequency within minium and maximum frequency.
*/
Maniac,
/**
* Synchronize changes of frequency with pulse width changes.
*/
Synchronized,
}
export interface ControlPoint {
x: number;
y: number;
}
export class Pulse {
order: number;
id: number;
duration: number;
width: number;
maniac: boolean;
frequency: number;
frequencyShifting: FrequencyShifting;
controlPoint1: ControlPoint;
controlPoint2: ControlPoint;
constructor(order: number, width: number, frequency: number) {
this.id = v4();
this.order = order;
this.duration = 25;
this.width = width;
this.maniac = false;
this.frequency = frequency;
this.frequencyShifting = FrequencyShifting.Linear;
this.controlPoint1 = { x: 0, y: 0 };
this.controlPoint2 = { x: 0, y: 0 };
}
equals(other: Pulse): boolean {
return this.id === other.id;
}
}
export class Pattern {
id: string;
name: string;
createdAt: number;
lastModifiedAt: number | null;
smoothRepeat: boolean;
pulses: Pulse[];
constructor() {
this.id = v4();
this.name = '';
this.createdAt = dayjs().valueOf();
this.lastModifiedAt = null;
this.smoothRepeat = true;
this.Pulses = [];
}
}
export function createNewPulse(pattern: Pattern): Pulse {
const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0);
if (pattern.smoothRepeat) {
return new Pulse(
maxOrder + 1,
get(pattern.pulses, '[0].width', 0),
get(pattern.pulses, '[0].frequency', 1),
);
} else {
return new Pulse(
maxOrder + 1,
get(pattern.pulses, '[-1].width', 0),
get(pattern.pulses, '[-1].frequency', 1),
);
}
}
export function totalDuration(pattern: Pattern): number {
return reduce(
pattern.smoothRepeat ? pattern.pulses : pattern.pulses.slice(0, -1),
(former, pulse) => former + pulse.duration,
0,
);
}
export const PatternsAtom = atomFamily((keyword: string) =>
atom(async () => {
try {
const patterns = await invoke<Pattern[]>('list_patterns', { keyword });
return patterns;
} catch (e) {
console.error('[retreiving pattern list]', e);
}
return [];
}),
);
export const CurrentPatternAtom = atom<Pattern | null>(null);
export const PulsesInCurrentPatternAtom = atom(
(get) => get(CurrentPatternAtom)?.pulses ?? [],
(get, set, pulse: Pulse) => {
const currentPulses = get(CurrentPatternAtom)?.pulses ?? [];
const newPulses = currentPulses.map((p) => (p.id === pulse.id ? pulse : p));
if (!newPulses.some((p) => p.id === pulse.id)) {
newPulses.push(pulse);
}
newPulses.sort((a, b) => a.order - b.order);
const currentPattern = get(CurrentPatternAtom);
if (currentPattern) {
set(CurrentPatternAtom, {
...currentPattern,
pulses: newPulses,
});
}
},
);
export const CurrentPatternDuration = atom((get) => {
const currentPattern = get(CurrentPatternAtom);
if (!currentPattern) return 0;
return totalDuration(currentPattern);
});