diff --git a/eslint.config.js b/eslint.config.js index 56841fa..40feae1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,7 +28,8 @@ export default tseslint.config( "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", "react-refresh/only-export-components": "off", - "prefer-const": "off" + "react-hooks/exhaustive-deps": "off", + "prefer-const": "off", }, }, ); diff --git a/package-lock.json b/package-lock.json index fd62ec8..34a4742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vestige", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vestige", - "version": "0.3.0", + "version": "0.4.0", "dependencies": { "@remixicon/react": "^4.5.0", "@tauri-apps/plugin-dialog": "^2.2.0", @@ -19,6 +19,7 @@ "postcss": "^8.4.49", "react": "^18.3.1", "react-dom": "^18.3.1", + "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.16", "tone": "^15.0.4" }, @@ -7091,6 +7092,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/package.json b/package.json index 175bb02..97a0b79 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vestige", "private": true, - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", @@ -22,6 +22,7 @@ "postcss": "^8.4.49", "react": "^18.3.1", "react-dom": "^18.3.1", + "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.16", "tone": "^15.0.4" }, diff --git a/public/samples/oneshots/707ch.wav b/public/samples/oneshots/707ch.wav new file mode 100644 index 0000000..a11f84c Binary files /dev/null and b/public/samples/oneshots/707ch.wav differ diff --git a/public/samples/oneshots/707cowbell.wav b/public/samples/oneshots/707cowbell.wav new file mode 100644 index 0000000..afa9467 Binary files /dev/null and b/public/samples/oneshots/707cowbell.wav differ diff --git a/public/samples/oneshots/707crash.wav b/public/samples/oneshots/707crash.wav new file mode 100644 index 0000000..32391e3 Binary files /dev/null and b/public/samples/oneshots/707crash.wav differ diff --git a/public/samples/oneshots/707kick.wav b/public/samples/oneshots/707kick.wav new file mode 100644 index 0000000..9d3fd19 Binary files /dev/null and b/public/samples/oneshots/707kick.wav differ diff --git a/public/samples/oneshots/707oh.wav b/public/samples/oneshots/707oh.wav new file mode 100644 index 0000000..506aa96 Binary files /dev/null and b/public/samples/oneshots/707oh.wav differ diff --git a/public/samples/oneshots/707ride.wav b/public/samples/oneshots/707ride.wav new file mode 100644 index 0000000..409f14c Binary files /dev/null and b/public/samples/oneshots/707ride.wav differ diff --git a/public/samples/oneshots/707snare.wav b/public/samples/oneshots/707snare.wav new file mode 100644 index 0000000..ada5200 Binary files /dev/null and b/public/samples/oneshots/707snare.wav differ diff --git a/public/samples/oneshots/707tamb.wav b/public/samples/oneshots/707tamb.wav new file mode 100644 index 0000000..fb56dc4 Binary files /dev/null and b/public/samples/oneshots/707tamb.wav differ diff --git a/public/samples/oneshots/808ch.wav b/public/samples/oneshots/808ch.wav new file mode 100644 index 0000000..abeb16c Binary files /dev/null and b/public/samples/oneshots/808ch.wav differ diff --git a/public/samples/oneshots/808cowbell.wav b/public/samples/oneshots/808cowbell.wav new file mode 100644 index 0000000..68bf9f9 Binary files /dev/null and b/public/samples/oneshots/808cowbell.wav differ diff --git a/public/samples/oneshots/808crash.wav b/public/samples/oneshots/808crash.wav new file mode 100644 index 0000000..764dd83 Binary files /dev/null and b/public/samples/oneshots/808crash.wav differ diff --git a/public/samples/oneshots/808oh.wav b/public/samples/oneshots/808oh.wav new file mode 100644 index 0000000..2711e5a Binary files /dev/null and b/public/samples/oneshots/808oh.wav differ diff --git a/public/samples/oneshots/808snare.wav b/public/samples/oneshots/808snare.wav new file mode 100644 index 0000000..366796c Binary files /dev/null and b/public/samples/oneshots/808snare.wav differ diff --git a/public/samples/oneshots/909ch.wav b/public/samples/oneshots/909ch.wav new file mode 100644 index 0000000..a1eebba Binary files /dev/null and b/public/samples/oneshots/909ch.wav differ diff --git a/public/samples/oneshots/909clap.wav b/public/samples/oneshots/909clap.wav new file mode 100644 index 0000000..a322674 Binary files /dev/null and b/public/samples/oneshots/909clap.wav differ diff --git a/public/samples/oneshots/909crash.wav b/public/samples/oneshots/909crash.wav new file mode 100644 index 0000000..00193cd Binary files /dev/null and b/public/samples/oneshots/909crash.wav differ diff --git a/public/samples/oneshots/909kick.wav b/public/samples/oneshots/909kick.wav new file mode 100644 index 0000000..e1209ff Binary files /dev/null and b/public/samples/oneshots/909kick.wav differ diff --git a/public/samples/oneshots/909oh.wav b/public/samples/oneshots/909oh.wav new file mode 100644 index 0000000..99aae50 Binary files /dev/null and b/public/samples/oneshots/909oh.wav differ diff --git a/public/samples/oneshots/909rim.wav b/public/samples/oneshots/909rim.wav new file mode 100644 index 0000000..68882ce Binary files /dev/null and b/public/samples/oneshots/909rim.wav differ diff --git a/public/samples/oneshots/909snare.wav b/public/samples/oneshots/909snare.wav new file mode 100644 index 0000000..323521b Binary files /dev/null and b/public/samples/oneshots/909snare.wav differ diff --git a/public/samples/oneshots/amenhat.wav b/public/samples/oneshots/amenhat.wav new file mode 100644 index 0000000..b5ce0c4 Binary files /dev/null and b/public/samples/oneshots/amenhat.wav differ diff --git a/public/samples/oneshots/amenkick1.wav b/public/samples/oneshots/amenkick1.wav new file mode 100644 index 0000000..bdc4ea0 Binary files /dev/null and b/public/samples/oneshots/amenkick1.wav differ diff --git a/public/samples/oneshots/amenkick2.wav b/public/samples/oneshots/amenkick2.wav new file mode 100644 index 0000000..0b3eeec Binary files /dev/null and b/public/samples/oneshots/amenkick2.wav differ diff --git a/public/samples/oneshots/amensnare.wav b/public/samples/oneshots/amensnare.wav new file mode 100644 index 0000000..24dd424 Binary files /dev/null and b/public/samples/oneshots/amensnare.wav differ diff --git a/public/samples/oneshots/wheelup.wav b/public/samples/oneshots/wheelup.wav new file mode 100644 index 0000000..6e430ee Binary files /dev/null and b/public/samples/oneshots/wheelup.wav differ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c6d8537..827b5a3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4516,7 +4516,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige" -version = "0.3.0" +version = "0.4.0" dependencies = [ "log", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 34cb417..b386c70 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige" -version = "0.3.0" +version = "0.4.0" description = "A generative music synthesizer" authors = ["ascpixi"] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e8e32f6..b1b176f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "vestige", - "version": "0.3.0", + "version": "0.4.0", "identifier": "dev.ascpixi.vestige", "build": { "frontendDist": "../dist", diff --git a/src/App.tsx b/src/App.tsx index b30f3a4..d12d01c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,39 +1,25 @@ import * as flow from "@xyflow/react"; import * as tone from "tone"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { RiDeleteBinFill, RiFileMusicFill, RiGraduationCapFill, RiImportFill, RiInformation2Fill, RiLinkM, RiLinkUnlinkM, RiPlayFill, RiSaveFill, RiStopFill } from "@remixicon/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { RiDeleteBinFill, RiFileCheckFill, RiFileMusicFill, RiGraduationCapFill, RiImportFill, RiInformation2Fill, RiLinkM, RiLinkUnlinkM, RiPlayFill, RiSaveFill, RiStopFill } from "@remixicon/react"; import "@xyflow/react/dist/style.css"; -import iconShadow from "./assets/icon-shadow.svg"; -import highSeasLogo from "./assets/highseas-logo.svg"; - -import { NodeTypeDescriptor, VESTIGE_NODE_SERIALIZERS, VESTIGE_NODE_TYPES, type VestigeNode } from "./nodes"; -import { PENTATONIC_MELODY_NODE_DESCRIPTOR } from "./nodes/PentatonicMelodyNode"; -import { FILTER_NODE_DESCRIPTOR } from "./nodes/FilterNode"; -import { SYNTH_NODE_DESCRIPTOR } from "./nodes/SynthNode"; -import { SAMPLER_NODE_DESCRIPTOR } from "./nodes/SamplerNode"; -import { LFO_NODE_DESCRIPTOR } from "./nodes/LfoNode"; -import { REVERB_NODE_DESCRIPTOR } from "./nodes/ReverbNode"; -import { BALANCE_NODE_DESCRIPTOR } from "./nodes/BalanceNode"; -import { MIX_NODE_DESCRIPTOR } from "./nodes/MixNode"; -import { DELAY_NODE_DESCRIPTOR } from "./nodes/DelayNode"; -import { PENTATONIC_CHORDS_NODE_DESCRIPTOR } from "./nodes/PentatonicChordsNode"; -import { PICK_NOTE_DESCRIPTOR } from "./nodes/PickNoteNode"; + +import { VESTIGE_NODE_SERIALIZERS, VESTIGE_NODE_TYPES, type VestigeNode } from "./nodes"; import { createFinalNode } from "./nodes/FinalNode"; -import { AbstractVestigeNode, AudioDestination, GraphForwarder, SIGNAL_INPUT_HID_PREFIX, SIGNAL_OUTPUT_HID, VALUE_INPUT_HID_PREFIX, VALUE_OUTPUT_HID } from "./graph"; -import { assert } from "./util"; -import { deserialize, deserializeBase64, serialize, serializeBase64 } from "./serializer"; -import { getPersistentData, mutatePersistentData } from "./persistent"; -import { AFTER_TOUR_PROJECT } from "./builtinProjects"; +import { VestigeGraph, GraphMutator, graphFromExisting } from "./graph"; +import { deserializeProject, deserializeBase64Project, serializeProject, serializeBase64Project } from "./serializer"; +import { getPersistentData } from "./persistent"; import { isTauri, promptToSaveFile } from "./environment"; -import { Link } from "./components/Link"; import { IntroductionTour } from "./components/IntroductionTour"; -import { ContextMenu, ContextMenuEntry } from "./components/ContextMenu"; import { EDGE_TYPES as VESTIGE_EDGE_TYPES } from "./components/VestigeEdge"; -import { CHORUS_NODE_DESCRIPTOR } from "./nodes/ChorusNode"; -import { ARPEGGIATOR_NOTE_DESCRIPTOR } from "./nodes/ArpeggiatorNode"; +import { AddNodeMenu } from "./components/app/AddNodeMenu"; +import { AppRenderDialog } from "./components/app/AppRenderDialog"; +import { AppAboutDialog } from "./components/app/AppAboutDialog"; +import { ConfirmationDialog } from "./components/app/ConfirmationDialog"; +import { AppProjectLinkDialog } from "./components/app/AppProjectLinkDialog"; const shouldShowTour = !getPersistentData().tourComplete; const shouldLoadExisting = location.hash.startsWith("#p:"); @@ -46,28 +32,60 @@ export default function App() { const [startTimeMs, setStartTimeMs] = useState(performance.now()); const [playing, setPlaying] = useState(false); - const [prevVolume, setPrevVolume] = useState(1); const [wasStarted, setWasStarted] = useState(false); const [connectedFinalBefore, setConnectedFinalBefore] = useState(false); - const [forwarder] = useState(new GraphForwarder()); - let [nodes, setNodes] = useState([]); - let [edges, setEdges] = useState([]); + let [graph, setGraph] = useState(new VestigeGraph()); const [graphVer, setGraphVer] = useState(0); const [ctxMenuPos, setCtxMenuPos] = useState({ x: 0, y: 0 }); const [showCtxMenu, setShowCtxMenu] = useState(false); - const [inTour, setInTour] = useState(false); const tourDialogRef = useRef(null); - const aboutDialogRef = useRef(null); const resetDialogRef = useRef(null); + const renderDialogRef = useRef(null); const [projLink, setProjLink] = useState(""); - const projLinkDialogRef = useRef(null); - const projLinkTextRef = useRef(null); + const [inTour, setInTour] = useState(false); + + const [realtimeCtx] = useState(tone.getContext()); + + const togglePlay = useCallback(async () => { + if (!playing) { + setStartTimeMs(performance.now()); + + if (!wasStarted) { + console.log("▶️ Playing (performing first time initialization)"); + await tone.start(); + setWasStarted(true); + } else { + console.log("▶️ Playing"); + tone.getDestination().volume.rampTo(tone.gainToDb(1), 0.25); + } + } else { + console.log("⏹️ Stopping"); + tone.getDestination().volume.rampTo(-Infinity, 0.25); + } + + setPlaying(!playing); + }, [playing, wasStarted]); + + const mutator = useMemo(() => new GraphMutator({ + onSignalConnect(_src, dst) { + if (dst.nodeType != "FINAL") + return; + + if (!connectedFinalBefore) { + if (!playing) { + togglePlay(); + } + + setConnectedFinalBefore(true); + } + } + }), [connectedFinalBefore, playing, togglePlay]); useEffect(() => { window.addEventListener("hashchange", () => { @@ -77,24 +95,18 @@ export default function App() { }); }, []); - const loadFromNodesAndEdges = useCallback((targetNodes: VestigeNode[], targetEdges: flow.Edge[]) => { - setNodes(targetNodes); - setEdges(targetEdges); + useEffect(() => { + if (!playing) + return; - // Even if we call "setNodes" and "setEdges" here, the state of those - // variables are still what they were previously (i.e. empty arrays in our case) - // on this render. We need to explicitly assign them. + const handler = () => { + const now = realtimeCtx.now(); + graph.nodes.forEach(x => x.data.onTick?.(now)); + }; - /* eslint-disable react-hooks/exhaustive-deps */ - nodes = targetNodes; - edges = targetEdges; - /* eslint-enable react-hooks/exhaustive-deps */ - - // We also need to handle all of the connections - for (const edge of targetEdges) { - onConnectChange(edge as flow.Connection, "CONNECT"); - } - }, []); + realtimeCtx.on("tick", handler); + return () => { realtimeCtx.off("tick", handler); } + }, [realtimeCtx, graphVer, playing]); useEffect(() => { if (!shouldLoadExisting) @@ -102,64 +114,33 @@ export default function App() { (async () => { const data = location.hash.substring(3); - const result = await deserializeBase64(data, VESTIGE_NODE_SERIALIZERS); + const result = await deserializeBase64Project(data, VESTIGE_NODE_SERIALIZERS); - loadFromNodesAndEdges(result.nodes, result.edges); + setGraph(graphFromExisting(result.nodes, result.edges)); console.log("✅ Successfully loaded project from URL.", result); location.hash = ""; })(); - }, [loadFromNodesAndEdges]); - - function getContextMenuEntry(descriptor: NodeTypeDescriptor): ContextMenuEntry { - return { - type: "ITEM", - content:
- {descriptor.icon("w-4 h-4")} - {descriptor.displayName} -
, - onChoose: async () => { - const { x, y } = thisFlow!.screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); - setNodes([...nodes, await descriptor.create(x - 200, y - 200)]); - setShowCtxMenu(false); - } - } - } + }, []); function handleContextMenuOpen(ev: React.MouseEvent) { - if (ev.pageX == 1 && ev.pageY == 1) { - // Touch devices seem to report the event originating at (1, 1) - in this - // case, we position the context menu at the top of the page, centered horizontally. - setCtxMenuPos({ - x: (document.body.clientWidth / 2) - 150, - y: 96 - }); - } else { - setCtxMenuPos({ x: ev.pageX, y: ev.pageY }); - } - + setCtxMenuPos({ x: ev.pageX, y: ev.pageY }); setShowCtxMenu(true); ev.preventDefault(); } async function saveAsLink() { - const data = await serializeBase64(nodes, edges, VESTIGE_NODE_SERIALIZERS); + const data = await serializeBase64Project(graph.nodes, graph.edges, VESTIGE_NODE_SERIALIZERS); const root = isTauri() ? "https://vestige.ascpixi.dev/" : location.origin + location.pathname; setProjLink(root + "#p:" + data); - projLinkDialogRef.current!.showModal(); - - setTimeout(() => { - projLinkTextRef.current!.focus(); - projLinkTextRef.current!.select(); - }, 50); } async function saveAsFile() { await promptToSaveFile( - await serialize(nodes, edges, VESTIGE_NODE_SERIALIZERS), + await serializeProject(graph.nodes, graph.edges, VESTIGE_NODE_SERIALIZERS), "untitled", "Vestige Project", "vestigeproj" @@ -186,8 +167,8 @@ export default function App() { reader.readAsArrayBuffer(file); }); - const result = await deserialize(new Uint8Array(data), VESTIGE_NODE_SERIALIZERS); - loadFromNodesAndEdges(result.nodes, result.edges); + const result = await deserializeProject(new Uint8Array(data), VESTIGE_NODE_SERIALIZERS); + setGraph(graphFromExisting(result.nodes, result.edges)); } function loadFromLink() { @@ -210,7 +191,7 @@ export default function App() { // Load default project setTimeout(() => { - setNodes([createFinalNode(0, 0)]); + setGraph(mutator.mutate(graph, { nodes: [createFinalNode(0, 0)] })); if (shouldShowTour) { tourDialogRef.current!.showModal(); @@ -218,146 +199,46 @@ export default function App() { }, 300); }, []); - const togglePlay = useCallback(async () => { - if (!playing) { - setStartTimeMs(performance.now()); - - if (!wasStarted) { - console.log("▶️ Playing (performing first time initialization)"); - await tone.start(); - setWasStarted(true); - } else { - console.log("▶️ Playing"); - tone.getDestination().volume.rampTo(prevVolume, 0.25); - } - } else { - console.log("⏹️ Stopping"); - setPrevVolume(tone.getDestination().volume.value); - tone.getDestination().volume.rampTo(-Infinity, 0.25); - } - - setPlaying(!playing); - }, [playing, prevVolume, wasStarted]); - const onNodesChange = useCallback( (changes: flow.NodeChange[]) => { - setNodes(nds => flow.applyNodeChanges(changes, nds)); + graph = mutator.mutate(graph, { nodes: flow.applyNodeChanges(changes, graph.nodes) }); + setGraph(graph); if (changes.some(x => x.type != "position")) { setGraphVer(x => x + 1); } }, - [], + [graph, mutator], ); - const onConnectChange = useCallback((conn: flow.Connection, action: "CONNECT" | "DISCONNECT") => { - const src = (nodes.find(x => x.id == conn.source)! as AbstractVestigeNode).data; - const dst = (nodes.find(x => x.id == conn.target)! as AbstractVestigeNode).data; - - // We only handle connection changes between Tone.js-backed nodes, such as - // INSTRUMENT or EFFECT. For NOTES and VALUE nodes, this is handled via the - // GraphForwarder. - if (conn.sourceHandle == SIGNAL_OUTPUT_HID && conn.targetHandle?.startsWith(SIGNAL_INPUT_HID_PREFIX)) { - // Main input/output change - if (src.nodeType == "NOTES" || dst.nodeType == "NOTES") - return; - - let connDest: AudioDestination; - if (dst.nodeType == "EFFECT") { - connDest = dst.effect.getConnectDestination(conn.targetHandle); - } else if (dst.nodeType == "FINAL") { - connDest = dst.getInputDestination(); - - if (!connectedFinalBefore) { - if (!playing) { - togglePlay(); - } - - setConnectedFinalBefore(true); - } - } else { - console.error("Invalid connection!", src, dst, conn); - throw new Error(`Attempted to connect a ${src.nodeType} node to a ${dst.nodeType} node`); - } - - if (action == "CONNECT") { - console.log("Connected:", src, " -> ", dst); - - if (src.nodeType == "INSTRUMENT") { - src.generator.connectTo(connDest); - console.log("Audio graph node changed:", src.generator, connDest); - } else if (src.nodeType == "EFFECT") { - src.effect.connectTo(connDest); - console.log("Audio graph node changed:", src.effect, connDest); - } - } else { - console.log("Disconnected:", src, " -> ", dst); - - if (src.nodeType == "INSTRUMENT") { - src.generator.disconnect(); - } else if (src.nodeType == "EFFECT") { - src.effect.disconnect(); - } - } - } else if (conn.sourceHandle == VALUE_OUTPUT_HID && conn.targetHandle?.startsWith(VALUE_INPUT_HID_PREFIX)) { - // Automatable parameter change - if (dst.nodeType == "EFFECT" || dst.nodeType == "INSTRUMENT") { - const automatable = dst.parameters[conn.targetHandle]; - - if (!automatable) { - console.error(`Attempted to automate parameter ${conn.targetHandle}, but there is no handle for it! Destination:`, dst); - throw new Error(`No automatable handle for ${conn.targetHandle} in ${conn.source}`); - } - - dst.parameters[conn.targetHandle].controlledBy = action == "CONNECT" - ? conn.source - : undefined; - } - } - }, [connectedFinalBefore, nodes, playing, togglePlay]); - - const onEdgesChange = useCallback( (changes: flow.EdgeChange[]) => { - for (const change of changes) { - if (change.type != "remove") - continue; - - const edge = edges.find(x => x.id == change.id); - assert(edge, `could not find removed edge with ID ${change.id}`); - - assert(edge.sourceHandle, `edge w/ ID ${change.id} has an undefined source handle`); - assert(edge.targetHandle, `edge w/ ID ${change.id} has an undefined target handle`) - - onConnectChange(edge as flow.Connection, "DISCONNECT"); - } - - setEdges(eds => flow.applyEdgeChanges(changes, eds)); + graph = mutator.mutateEdges(graph, changes); + setGraph(graph); setGraphVer(x => x + 1); }, - [edges, onConnectChange] + [graph, mutator] ); const onConnect = useCallback( (params: flow.Connection) => { // We can't connect two different sources to one target - if (edges.some(x => x.target == params.target && x.targetHandle == params.targetHandle)) + if (graph.edges.some(x => x.target == params.target && x.targetHandle == params.targetHandle)) return; - setEdges(eds => flow.addEdge({ ...params, type: "vestige" }, eds)); - onConnectChange(params, "CONNECT"); + setGraph(mutator.mutate(graph, { + edges: flow.addEdge({ ...params, type: "vestige" }, graph.edges) + })); + setGraphVer(x => x + 1); }, - [edges, onConnectChange] + [graph, mutator] ); useEffect(() => { if (playing) { const id = setInterval(() => { - forwarder.traceGraph( - (performance.now() - startTimeMs) / 1000, - nodes, edges - ); + graph.traceGraph((performance.now() - startTimeMs) / 1000); }, (1 / 96) * 1000); return () => clearInterval(id); @@ -367,176 +248,57 @@ export default function App() { // way - `nodes` changes when a node is re-positioned, and we really don't need to reset // the interval just for that (we don't care about node positions!) - we instead keep // a "graph version" counter, which we increment every time the graph changes in a way - // that concerns us. We provide it as a dependency. Applying what the exhaustive - // dependency warning tells us to do would actually do more harm than good. - // - // eslint-disable-next-line react-hooks/exhaustive-deps - [forwarder, graphVer, startTimeMs, playing] + // that concerns us. We provide it as a dependency. + [graphVer, startTimeMs, playing] ); - function handleTourFinished() { - mutatePersistentData({ tourComplete: true }); - location.hash = `#p:${AFTER_TOUR_PROJECT}`; - } - - function enterTour() { - if (nodes.length != 1 || edges.length != 0) { - mutatePersistentData({ tourComplete: false }); - location.href = "#tour"; - } - - setInTour(true); - } - - function skipTour() { - mutatePersistentData({ tourComplete: true }); - } return (
-
- (document.body.clientWidth * .65) ? "LEFT" : "RIGHT"} - entries={[ - { - type: "GROUP", content: "melodies & chords", entries: [ - getContextMenuEntry(PENTATONIC_MELODY_NODE_DESCRIPTOR), - getContextMenuEntry(PENTATONIC_CHORDS_NODE_DESCRIPTOR), - getContextMenuEntry(ARPEGGIATOR_NOTE_DESCRIPTOR), - getContextMenuEntry(PICK_NOTE_DESCRIPTOR), - ] - }, - { - type: "GROUP", content: "instruments", entries: [ - getContextMenuEntry(SYNTH_NODE_DESCRIPTOR), - getContextMenuEntry(SAMPLER_NODE_DESCRIPTOR) - ] - }, - { - type: "GROUP", content: "effects", entries: [ - getContextMenuEntry(FILTER_NODE_DESCRIPTOR), - getContextMenuEntry(REVERB_NODE_DESCRIPTOR), - getContextMenuEntry(DELAY_NODE_DESCRIPTOR), - getContextMenuEntry(CHORUS_NODE_DESCRIPTOR) - ] - }, - getContextMenuEntry(LFO_NODE_DESCRIPTOR), - getContextMenuEntry(MIX_NODE_DESCRIPTOR), - getContextMenuEntry(BALANCE_NODE_DESCRIPTOR) - ]}/> -
+ { + const { x, y } = thisFlow!.screenToFlowPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2 + }); + + setGraph(mutator.addNode(graph, await descriptor.create(x - 200, y - 200))); + setShowCtxMenu(false); + }} + /> { - !thisFlow || !inTour ? <> : -
- -
+ !thisFlow ? <> : + } - -
-

Project link

-

- This is a link to your project - whenever you'll open it, this - version of the project will be restored. -

- -