From 4a2f599bff9090eeb5f507c8968b53fba5070dab Mon Sep 17 00:00:00 2001 From: Vixalie Date: Thu, 6 Mar 2025 14:29:32 +0800 Subject: [PATCH] add Pattern Context. --- src/context/Patterns.tsx | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/context/Patterns.tsx diff --git a/src/context/Patterns.tsx b/src/context/Patterns.tsx new file mode 100644 index 0000000..f612ece --- /dev/null +++ b/src/context/Patterns.tsx @@ -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('list_patterns', { keyword }); + return patterns; + } catch (e) { + console.error('[retreiving pattern list]', e); + } + return []; + }), +); +export const CurrentPatternAtom = atom(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); +});