Skip to content

Commit

Permalink
✨ feat(ims-view-pc): add Flow
Browse files Browse the repository at this point in the history
  • Loading branch information
eternallycyf committed Sep 7, 2024
1 parent 7654dac commit c1a0740
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 12 deletions.
5 changes: 3 additions & 2 deletions packages/ims-view-pc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"open-docxtemplater-image-module": "^1.0.3",
"pizzip": "^3.1.7",
"react-file-viewer": "^1.2.1",
"rc-tree": "^5.8.7"
"rc-tree": "^5.8.7",
"@xyflow/react": "^12.2.0"
},
"peerDependencies": {
"@ant-design/icons": "^5.2.6",
Expand All @@ -63,4 +64,4 @@
"access": "public",
"registry": "https://registry.npmjs.org"
}
}
}
82 changes: 82 additions & 0 deletions packages/ims-view-pc/src/components/Flow/Audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const context = new AudioContext();

const osc = context.createOscillator();
osc.frequency.value = 220;
osc.type = 'square';
osc.start();

const volume = context.createGain();
volume.gain.value = 0.5;

const out = context.destination;

const nodes = new Map();

nodes.set('a', osc);
nodes.set('b', volume);
nodes.set('c', out);

export function isRunning() {
return context.state === 'running';
}

export function toggleAudio() {
return isRunning() ? context.suspend() : context.resume();
}

export function updateAudioNode(id: string, data: Record<string, any>) {
const node = nodes.get(id);

for (const [key, val] of Object.entries(data)) {
if (node[key] instanceof AudioParam) {
node[key].value = val;
} else {
node[key] = val;
}
}
}

export function removeAudioNode(id: string) {
const node = nodes.get(id);

node.disconnect();
node.stop?.();

nodes.delete(id);
}

export function connect(sourceId: string, targetId: string) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);

window.osc = osc;
source.connect(target);
}

export function disconnect(sourceId: string, targetId: string) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.disconnect(target);
}

export function createAudioNode(id: string, type: string, data: Record<string, any>) {
switch (type) {
case 'osc': {
const node = context.createOscillator();
node.frequency.value = data.frequency;
node.type = data.type;
node.start();

nodes.set(id, node);
break;
}

case 'volume': {
const node = context.createGain();
node.gain.value = data.gain;

nodes.set(id, node);
break;
}
}
}
110 changes: 110 additions & 0 deletions packages/ims-view-pc/src/components/Flow/Flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
addEdge,
Background,
BackgroundVariant,
Connection,
Controls,
Edge,
EdgeTypes,
MiniMap,
Node,
OnConnect,
Panel,
ReactFlow,
useEdgesState,
useNodesState,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { connect, createAudioNode } from './Audio';
import { OscillatorNode } from './components/OscillatorNode';
import { OutputNode } from './components/OutputNode';
import { VolumeNode } from './components/VolumeNode';

const initialNodes: Node[] = [
{
id: 'a',
type: 'osc',
data: { frequency: 220, type: 'square' },
position: { x: 200, y: 0 },
},
{
id: 'b',
type: 'volume',
data: { gain: 0.5 },
position: { x: 150, y: 250 },
},
{
id: 'c',
type: 'out',
data: {},
position: { x: 350, y: 400 },
},
];

const initialEdges: Edge[] = [];

const nodeTypes = {
osc: OscillatorNode,
volume: VolumeNode,
out: OutputNode,
};

export default function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

const onConnect = (params: Connection) => {
connect(params.source, params.target);
setEdges((eds) => addEdge(params, eds));
};

function addOscNode() {
const id = Math.random().toString().slice(2, 8);
const position = { x: 0, y: 0 };
const type = 'osc';
const data = { frequency: 400, type: 'sine' };

setNodes([...nodes, { id, type, data, position }]);
createAudioNode(id, type, data);
}

function addVolumeNode() {
const id = Math.random().toString().slice(2, 8);
const data = { gain: 0.5 };
const position = { x: 0, y: 0 };
const type = 'volume';

setNodes([...nodes, { id, type, data, position }]);
createAudioNode(id, type, data);
}

return (
<div style={{ width: 'auto', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
>
<Controls />
<MiniMap />
<Background variant={BackgroundVariant.Lines} />
<Panel className={'space-x-4'} position="top-right">
<button type="button" className={'p-[4px] rounded bg-white shadow'} onClick={addOscNode}>
添加振荡器节点
</button>
<button
type="button"
className={'p-[4px] rounded bg-white shadow'}
onClick={addVolumeNode}
>
添加音量节点
</button>
</Panel>
</ReactFlow>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Handle, Position, useReactFlow } from '@xyflow/react';
import { ChangeEvent, ChangeEventHandler, useState } from 'react';
import { updateAudioNode } from '../Audio';

export interface OscillatorNodeProps {
id: string;
data: {
frequency: number;
type: string;
};
}

export function OscillatorNode({ id, data }: OscillatorNodeProps) {
const [frequency, setFrequency] = useState(data.frequency);
const [type, setType] = useState(data.type);

const changeFrequency: ChangeEventHandler<HTMLInputElement> = (e) => {
setFrequency(+e.target.value);
updateAudioNode(id, { frequency: +e.target.value });
};

const changeType: ChangeEventHandler<HTMLSelectElement> = (e) => {
setType(e.target.value);
updateAudioNode(id, { type: e.target.value });
};

return (
<div className={'bg-white shadow-xl'}>
<p className={'rounded-t-md p-[8px] bg-pink-500 text-white'}>振荡器节点</p>
<div className={'flex flex-col p-[8px]'}>
<span>频率</span>
<input
className="nodrag"
type="range"
min="10"
max="1000"
value={frequency}
onChange={changeFrequency}
/>
<span className={'text-right'}>{frequency}赫兹</span>
</div>
<hr className={'mx-[4px]'} />
<div className={'flex flex-col p-[8px]'}>
<p>波形</p>
<select value={type} onChange={changeType}>
<option value="sine">正弦波</option>
<option value="triangle">三角波</option>
<option value="sawtooth">锯齿波</option>
<option value="square">方波</option>
</select>
</div>
<Handle className="w-[10px] h-[10px]" type="source" position={Position.Bottom} />
</div>
);
}
26 changes: 26 additions & 0 deletions packages/ims-view-pc/src/components/Flow/components/OutputNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Handle, Position } from '@xyflow/react';
import { useState } from 'react';
import { toggleAudio } from '../Audio';

export function OutputNode() {
const [isRunning, setIsRuning] = useState(false);

return (
<div className={'bg-white shadow-xl p-[20px]'}>
<Handle className="w-[10px] h-[10px]" type="target" position={Position.Top} />

<div>
<p>输出节点</p>
<button
type="button"
onClick={() => {
setIsRuning((isRunning) => !isRunning);
toggleAudio();
}}
>
{isRunning ? <span role="img">🔈</span> : <span role="img">🔇</span>}
</button>
</div>
</div>
);
}
42 changes: 42 additions & 0 deletions packages/ims-view-pc/src/components/Flow/components/VolumeNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Handle, Position } from '@xyflow/react';
import { ChangeEvent, ChangeEventHandler, useState } from 'react';
import { updateAudioNode } from '../Audio';

export interface VolumeNodeProps {
id: string;
data: {
gain: number;
};
}

export function VolumeNode({ id, data }: VolumeNodeProps) {
const [gain, setGain] = useState(data.gain);

const changeGain: ChangeEventHandler<HTMLInputElement> = (e) => {
setGain(+e.target.value);
updateAudioNode(id, { gain: +e.target.value });
};

return (
<div className={'rounded-md bg-white shadow-xl'}>
<Handle type="target" className="w-[10px] h-[10px]" position={Position.Top} />

<p className={'rounded-t-md p-[4px] bg-blue-500 text-white'}>音量节点</p>
<div className={'flex flex-col p-[4px]'}>
<p>Gain</p>
<input
className="nodrag"
type="range"
min="0"
max="1"
step="0.01"
value={gain}
onChange={changeGain}
/>
<p className={'text-right'}>{gain.toFixed(2)}</p>
</div>

<Handle type="source" className="w-[10px] h-[10px]" position={Position.Bottom} />
</div>
);
}
11 changes: 11 additions & 0 deletions packages/ims-view-pc/src/components/Flow/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Flow } from 'ims-view-pc';
import React from 'react';

const Demo = () => {
return (
<>
<Flow />
</>
);
};
export default Demo;
13 changes: 13 additions & 0 deletions packages/ims-view-pc/src/components/Flow/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Flow
description: 流程图
group:
title: 其他
toc: content
---

## Flow 流程图

## 示例

<code transform="true" src="./demo/index.tsx">Flow</code>
2 changes: 2 additions & 0 deletions packages/ims-view-pc/src/components/Flow/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Flow from './Flow';
export default Flow;
1 change: 1 addition & 0 deletions packages/ims-view-pc/src/components/Flow/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export interface FlowProps {}
2 changes: 2 additions & 0 deletions packages/ims-view-pc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export * from './components/ExpandSelect';
export { default as ExpandSelect } from './components/ExpandSelect';
export { default as ExportButton } from './components/ExportButton';
export * from './components/ExportButton/interface';
export { default as Flow } from './components/Flow';
export * from './components/Flow/interface';
export { default as Icon } from './components/Icon';
export { default as Marker } from './components/Marker';
export * from './components/Marker/interface';
Expand Down
Loading

0 comments on commit c1a0740

Please sign in to comment.