From 3253b8b98e1fb556961dc60b7cf98bda6b3ba4a4 Mon Sep 17 00:00:00 2001 From: Vixalie Date: Fri, 21 Mar 2025 14:25:03 +0800 Subject: [PATCH] refactor Pattern and Pulse methods. --- src/context/Patterns.tsx | 173 +---------------- src/context/pattern-model.ts | 176 ++++++++++++++++++ .../pattern-editor/PulseCard.tsx | 12 ++ .../pattern-library/PatternDetail.tsx | 9 +- .../pattern-library/Patterns.tsx | 9 +- src/pages/CreatePattern.tsx | 2 +- 6 files changed, 194 insertions(+), 187 deletions(-) create mode 100644 src/context/pattern-model.ts create mode 100644 src/page-components/pattern-editor/PulseCard.tsx diff --git a/src/context/Patterns.tsx b/src/context/Patterns.tsx index a32c07a..537414c 100644 --- a/src/context/Patterns.tsx +++ b/src/context/Patterns.tsx @@ -1,180 +1,9 @@ import { invoke } from '@tauri-apps/api/core'; -import dayjs from 'dayjs'; import { atom, useAtomValue, useSetAtom } from 'jotai'; import { atomWithRefresh } from 'jotai/utils'; -import { get, reduce } from 'lodash-es'; import { useCallback } from 'react'; -import { v4 } from 'uuid'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; - -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; - offset: 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.offset = 0; - 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 = []; - } - - movePulseUp(pulseId: string, step: number) { - const index = this.pulses.findIndex((pulse) => pulse.id === pulseId); - if (index === -1 || index - step < 0) return; - - const targetIndex = index - step; - const targetPulse = this.pulses[targetIndex]; - const currentPulse = this.pulses[index]; - - // Swap the pulses - this.pulses[targetIndex] = currentPulse; - this.pulses[index] = targetPulse; - - // Swap their order - const tempOrder = currentPulse.order; - currentPulse.order = targetPulse.order; - targetPulse.order = tempOrder; - - // Sort pulses by order - this.pulses.sort((a, b) => a.order - b.order); - } - - movePulseDown(pulseId: string, step: number) { - const index = this.pulses.findIndex((pulse) => pulse.id === pulseId); - if (index === -1 || index + step >= this.pulses.length) return; - - const targetIndex = index + step; - const targetPulse = this.pulses[targetIndex]; - const currentPulse = this.pulses[index]; - - // Swap the pulses - this.pulses[targetIndex] = currentPulse; - this.pulses[index] = targetPulse; - - // Swap their order - const tempOrder = currentPulse.order; - currentPulse.order = targetPulse.order; - targetPulse.order = tempOrder; - - // Sort pulses by order - this.pulses.sort((a, b) => a.order - b.order); - } - - addPulse(): Pulse { - const maxOrder = reduce(this.pulses, (former, pulse) => Math.max(former, pulse.order), 0); - const newPulse = new Pulse( - maxOrder + 1, - this.smoothRepeat ? get(this.pulses, '[0].width', 0) : get(this.pulses, '[-1].width', 0), - this.smoothRepeat - ? get(this.pulses, '[0].frequency', 1) - : get(this.pulses, '[-1].frequency', 1), - ); - this.pulses.push(newPulse); - return newPulse; - } - - updatePulse(pulseId: string, pulse: Pulse) { - const index = this.pulses.findIndex((p) => p.id === pulseId); - if (index !== -1) { - const { id, order, ...rest } = pulse; - this.pulses[index] = { ...this.pulses[index], ...rest }; - } - } - - deletePulse(pulseId: string) { - this.pulses = this.pulses.filter((pulse) => pulse.id !== pulseId); - this.pulses.sort((a, b) => a.order - b.order); - this.pulses.forEach((pulse, index) => { - pulse.order = index + 1; - }); - } -} - -export function totalDuration(pattern: Pattern): number { - return reduce( - pattern.pulses, - (former, pulse) => former + pulse.offset, - pattern.smoothRepeat && pattern.pulses.length > 1 ? 100 : 0, - ); -} +import { Pattern, Pulse, totalDuration } from './pattern-model'; export const SearchKeywordAtom = atom(null); export const PatternsAtom = atomWithRefresh(async (get) => { diff --git a/src/context/pattern-model.ts b/src/context/pattern-model.ts new file mode 100644 index 0000000..e26ef6f --- /dev/null +++ b/src/context/pattern-model.ts @@ -0,0 +1,176 @@ +import dayjs from 'dayjs'; +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 interface Pulse { + order: number; + id: number; + offset: number; + width: number; + maniac: boolean; + frequency: number; + frequencyShifting: FrequencyShifting; + controlPoint1: ControlPoint; + controlPoint2: ControlPoint; +} +export function createPulse(order: number, width: number, frequency: number) { + return { + id: v4(), + order, + offset: 0, + width, + maniac: false, + frequency, + frequencyShifting: FrequencyShifting.Linear, + controlPoint1: { x: 0, y: 0 }, + controlPoint2: { x: 0, y: 0 }, + } as Pulse; +} + +export interface Pattern { + id: string; + name: string; + createdAt: number; + lastModifiedAt: number | null; + smoothRepeat: boolean; + pulses: Pulse[]; +} +export function createPattern() { + return { + id: v4(), + name: '', + createdAt: dayjs().valueOf(), + lastModifiedAt: null, + smoothRepeat: true, + pulses: [], + } as Pattern; +} + +export function movePulseUp(pattern: Pattern, pulseId: string, step: number) { + const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId); + if (index === -1 || index - step < 0) return; + + const targetIndex = index - step; + const targetPulse = pattern.pulses[targetIndex]; + const currentPulse = pattern.pulses[index]; + + // Swap the pulses + pattern.pulses[targetIndex] = currentPulse; + pattern.pulses[index] = targetPulse; + + // Swap their order + const tempOrder = currentPulse.order; + currentPulse.order = targetPulse.order; + targetPulse.order = tempOrder; + + // Sort pulses by order + pattern.pulses.sort((a, b) => a.order - b.order); +} + +export function movePulseDown(pattern: Pattern, pulseId: string, step: number) { + const index = pattern.pulses.findIndex((pulse) => pulse.id === pulseId); + if (index === -1 || index + step >= pattern.pulses.length) return; + + const targetIndex = index + step; + const targetPulse = pattern.pulses[targetIndex]; + const currentPulse = pattern.pulses[index]; + + // Swap the pulses + pattern.pulses[targetIndex] = currentPulse; + pattern.pulses[index] = targetPulse; + + // Swap their order + const tempOrder = currentPulse.order; + currentPulse.order = targetPulse.order; + targetPulse.order = tempOrder; + + // Sort pulses by order + pattern.pulses.sort((a, b) => a.order - b.order); +} + +export function addPulse(pattern: Pattern): Pulse { + const maxOrder = reduce(pattern.pulses, (former, pulse) => Math.max(former, pulse.order), 0); + const newPulse = createPulse( + maxOrder + 1, + pattern.smoothRepeat + ? get(pattern.pulses, '[0].width', 0) + : get(pattern.pulses, '[-1].width', 0), + pattern.smoothRepeat + ? get(pattern.pulses, '[0].frequency', 1) + : get(pattern.pulses, '[-1].frequency', 1), + ); + pattern.pulses.push(newPulse); + return newPulse; +} + +export function updatePulse(pattern: Pattern, pulseId: string, pulse: Pulse) { + const index = pattern.pulses.findIndex((p) => p.id === pulseId); + if (index !== -1) { + const { id, order, ...rest } = pulse; + pattern.pulses[index] = { ...pattern.pulses[index], ...rest }; + } +} + +export function deletePulse(pattern: Pattern, pulseId: string) { + pattern.pulses = pattern.pulses.filter((pulse) => pulse.id !== pulseId); + pattern.pulses.sort((a, b) => a.order - b.order); + pattern.pulses.forEach((pulse, index) => { + pulse.order = index + 1; + }); +} + +export function durationRemains(pattern: Pattern): number { + return 20 * 1000 - totalDuration(pattern) - (pattern.smoothRepeat ? 100 : 0); +} + +export function totalDuration(pattern: Pattern): number { + return reduce( + pattern.pulses, + (former, pulse) => former + pulse.offset, + pattern.smoothRepeat && pattern.pulses.length > 1 ? 100 : 0, + ); +} diff --git a/src/page-components/pattern-editor/PulseCard.tsx b/src/page-components/pattern-editor/PulseCard.tsx new file mode 100644 index 0000000..0c9d85a --- /dev/null +++ b/src/page-components/pattern-editor/PulseCard.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import { Pulse } from '../../context/pattern-model'; + +type PulseCardProps = { + pulse: Pulse; +}; + +const PulseCard: FC = ({ pulse }) => { + return
; +}; + +export default PulseCard; diff --git a/src/page-components/pattern-library/PatternDetail.tsx b/src/page-components/pattern-library/PatternDetail.tsx index 4f42c2a..8837c7c 100644 --- a/src/page-components/pattern-library/PatternDetail.tsx +++ b/src/page-components/pattern-library/PatternDetail.tsx @@ -6,13 +6,8 @@ import { FC, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { NotificationType, useNotification } from '../../components/Notifications'; import PatternPreview from '../../components/PatternPreview'; -import { - CurrentPatternAtom, - Pattern, - PatternsAtom, - SelectedPatternIdAtom, - totalDuration, -} from '../../context/Patterns'; +import { CurrentPatternAtom, PatternsAtom, SelectedPatternIdAtom } from '../../context/Patterns'; +import { Pattern, totalDuration } from '../../context/pattern-model'; import styles from './PatternDetail.module.css'; const EmptyPromption: FC = () => { diff --git a/src/page-components/pattern-library/Patterns.tsx b/src/page-components/pattern-library/Patterns.tsx index 5ed5cb3..4bc0bfa 100644 --- a/src/page-components/pattern-library/Patterns.tsx +++ b/src/page-components/pattern-library/Patterns.tsx @@ -5,13 +5,8 @@ import { FC, useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDebounce } from 'react-use'; import { ScrollArea } from '../../components/ScrollArea'; -import { - Pattern, - PatternsAtom, - SearchKeywordAtom, - SelectedPatternIdAtom, - totalDuration, -} from '../../context/Patterns'; +import { PatternsAtom, SearchKeywordAtom, SelectedPatternIdAtom } from '../../context/Patterns'; +import { Pattern, totalDuration } from '../../context/pattern-model'; import styles from './Patterns.module.css'; const PatternCard: FC<{ pattern: Pattern }> = ({ pattern }) => { diff --git a/src/pages/CreatePattern.tsx b/src/pages/CreatePattern.tsx index 16af4a1..c7dc2ae 100644 --- a/src/pages/CreatePattern.tsx +++ b/src/pages/CreatePattern.tsx @@ -3,7 +3,7 @@ import { useSetAtom } from 'jotai'; import { FC, useActionState } from 'react'; import { useNavigate } from 'react-router-dom'; import { NotificationType, ToastDuration, useNotification } from '../components/Notifications'; -import { Pattern, SelectedPatternIdAtom, useSavePattern } from '../context/Patterns'; +import { SelectedPatternIdAtom, useSavePattern } from '../context/Patterns'; import styles from './CreatePattern.module.css'; const CreatePattern: FC = () => {