fix merge

This commit is contained in:
Sam Lavigne 2023-08-24 23:20:31 -04:00
commit 7eca60a425
16 changed files with 1345 additions and 211 deletions

416
package-lock.json generated
View File

@ -14,8 +14,10 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2", "@sveltejs/vite-plugin-svelte": "^2.4.2",
"@xyflow/svelte": "^0.0.12",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-dnd-action": "^0.9.25", "svelte-dnd-action": "^0.9.25",
"svelvet": "^8.1.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vite": "^4.4.5" "vite": "^4.4.5"
} }
@ -465,6 +467,12 @@
"resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-1.0.8.tgz",
"integrity": "sha512-HQ6aJlYpWLq1f9AiApJl0aOIXlJUtuhBOYfSfv5rt3XNYkCBveojtnL6FvOVpJ2gEJ2wqgMW8xOHkLVYAbXghg==" "integrity": "sha512-HQ6aJlYpWLq1f9AiApJl0aOIXlJUtuhBOYfSfv5rt3XNYkCBveojtnL6FvOVpJ2gEJ2wqgMW8xOHkLVYAbXghg=="
}, },
"node_modules/@svelte-put/shortcut": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.0.0.tgz",
"integrity": "sha512-nZg3pwpTi9wUsvQPlqOzEsxZcF2jmY5j+VBq/20IUjjd2OpM92XqZAga0PCCjE6OuEobOt58UMnC2QZgOvk0tQ==",
"dev": true
},
"node_modules/@sveltejs/vite-plugin-svelte": { "node_modules/@sveltejs/vite-plugin-svelte": {
"version": "2.4.5", "version": "2.4.5",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.5.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.5.tgz",
@ -504,12 +512,300 @@
"vite": "^4.0.0" "vite": "^4.0.0"
} }
}, },
"node_modules/@types/d3": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz",
"integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.6.tgz",
"integrity": "sha512-NHkizg870sKYQn45oZT5ItoHqcgRgJD7KAiWZp4Udc6YdrFH2W0tZ2vv4shRHP+SXHoJ1G8B4I1GWb5oQSGypA==",
"dev": true
},
"node_modules/@types/d3-axis": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.3.tgz",
"integrity": "sha512-SE3x/pLO/+GIHH17mvs1uUVPkZ3bHquGzvZpPAh4yadRy71J93MJBpgK/xY8l9gT28yTN1g9v3HfGSFeBMmwZw==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.3.tgz",
"integrity": "sha512-MQ1/M/B5ifTScHSe5koNkhxn2mhUPqXjGuKjjVYckplAPjP9t2I2sZafb/YVHDwhoXWZoSav+Q726eIbN3qprA==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.3.tgz",
"integrity": "sha512-keuSRwO02c7PBV3JMWuctIfdeJrVFI7RpzouehvBWL4/GGUB3PBNg/9ZKPZAgJphzmS2v2+7vr7BGDQw1CAulw==",
"dev": true
},
"node_modules/@types/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==",
"dev": true
},
"node_modules/@types/d3-contour": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.3.tgz",
"integrity": "sha512-x7G/tdDZt4m09XZnG2SutbIuQqmkNYqR9uhDMdPlpJbcwepkEjEWG29euFcgVA1k6cn92CHdDL9Z+fOnxnbVQw==",
"dev": true,
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz",
"integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==",
"dev": true
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.3.tgz",
"integrity": "sha512-Df7KW3Re7G6cIpIhQtqHin8yUxUHYAqiE41ffopbmU5+FifYUNV7RVyTg8rQdkEagg83m14QtS8InvNb95Zqug==",
"dev": true
},
"node_modules/@types/d3-drag": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.3.tgz",
"integrity": "sha512-82AuQMpBQjuXeIX4tjCYfWjpm3g7aGCfx6dFlxX2JlRaiME/QWcHzBsINl7gbHCODA2anPYlL31/Trj/UnjK9A==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-dsv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.2.tgz",
"integrity": "sha512-DooW5AOkj4AGmseVvbwHvwM/Ltu0Ks0WrhG6r5FG9riHT5oUUTHz6xHsHqJSVU8ZmPkOqlUEY2obS5C9oCIi2g==",
"dev": true
},
"node_modules/@types/d3-ease": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz",
"integrity": "sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==",
"dev": true
},
"node_modules/@types/d3-fetch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.3.tgz",
"integrity": "sha512-/EsDKRiQkby3Z/8/AiZq8bsuLDo/tYHnNIZkUpSeEHWV7fHUl6QFBjvMPbhkKGk9jZutzfOkGygCV7eR/MkcXA==",
"dev": true,
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.5.tgz",
"integrity": "sha512-EGG+IWx93ESSXBwfh/5uPuR9Hp8M6o6qEGU7bBQslxCvrdUBQZha/EFpu/VMdLU4B0y4Oe4h175nSm7p9uqFug==",
"dev": true
},
"node_modules/@types/d3-format": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz",
"integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==",
"dev": true
},
"node_modules/@types/d3-geo": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.4.tgz",
"integrity": "sha512-kmUK8rVVIBPKJ1/v36bk2aSgwRj2N/ZkjDT+FkMT5pgedZoPlyhaG62J+9EgNIgUXE6IIL0b7bkLxCzhE6U4VQ==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.3.tgz",
"integrity": "sha512-GpSK308Xj+HeLvogfEc7QsCOcIxkDwLhFYnOoohosEzOqv7/agxwvJER1v/kTC+CY1nfazR0F7gnHo7GE41/fw==",
"dev": true
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==",
"dev": true,
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz",
"integrity": "sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==",
"dev": true
},
"node_modules/@types/d3-polygon": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz",
"integrity": "sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==",
"dev": true
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz",
"integrity": "sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==",
"dev": true
},
"node_modules/@types/d3-random": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==",
"dev": true
},
"node_modules/@types/d3-scale": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.4.tgz",
"integrity": "sha512-eq1ZeTj0yr72L8MQk6N6heP603ubnywSDRfNpi5enouR112HzGLS6RIvExCzZTraFF4HdzNpJMwA/zGiMoHUUw==",
"dev": true,
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz",
"integrity": "sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==",
"dev": true
},
"node_modules/@types/d3-selection": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.6.tgz",
"integrity": "sha512-2ACr96USZVjXR9KMD9IWi1Epo4rSDKnUtYn6q2SPhYxykvXTw9vR77lkFNruXVg4i1tzQtBxeDMx0oNvJWbF1w==",
"dev": true
},
"node_modules/@types/d3-shape": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.2.tgz",
"integrity": "sha512-NN4CXr3qeOUNyK5WasVUV8NCSAx/CRVcwcb0BuuS1PiTqwIm6ABi1SyasLZ/vsVCFDArF+W4QiGzSry1eKYQ7w==",
"dev": true,
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz",
"integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==",
"dev": true
},
"node_modules/@types/d3-time-format": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz",
"integrity": "sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==",
"dev": true
},
"node_modules/@types/d3-timer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz",
"integrity": "sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==",
"dev": true
},
"node_modules/@types/d3-transition": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.4.tgz",
"integrity": "sha512-512a4uCOjUzsebydItSXsHrPeQblCVk8IKjqCUmrlvBWkkVh3donTTxmURDo1YPwIVDh5YVwCAO6gR4sgimCPQ==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.4.tgz",
"integrity": "sha512-cqkuY1ah9ZQre2POqjSLcM8g40UVya/qwEUrNYP2/rCVljbmqKCVcv+ebvwhlI5azIbSEL7m+os6n+WlYA43aA==",
"dev": true,
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"dev": true "dev": true
}, },
"node_modules/@types/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==",
"dev": true
},
"node_modules/@xyflow/svelte": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.0.12.tgz",
"integrity": "sha512-BXwOauzjInjPBdJb1GaKVCYIxyznQKzuEltnPb3i3/EO9GJHx1xcD/c8aBUBBnAG7EWBKtuiyDGk/EBW8MEmsg==",
"dev": true,
"dependencies": {
"@svelte-put/shortcut": "^3.0.0",
"@xyflow/system": "0.0.2",
"classcat": "^5.0.4"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.2.tgz",
"integrity": "sha512-xtDf35E4Bwmt9IutmhNLna/vMNN3uRA/kwYUYTAifnWSCUaKENBqMOX4xAGNljBJX1yjo7FNlMRgn347raN69Q==",
"dev": true,
"dependencies": {
"@types/d3": "^7.4.0",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
@ -540,6 +836,12 @@
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
}, },
"node_modules/classcat": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz",
"integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==",
"dev": true
},
"node_modules/code-red": { "node_modules/code-red": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
@ -566,6 +868,111 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
} }
}, },
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dev": true,
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dev": true,
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dev": true,
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dev": true,
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -842,6 +1249,15 @@
"svelte": "^3.19.0 || ^4.0.0" "svelte": "^3.19.0 || ^4.0.0"
} }
}, },
"node_modules/svelvet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/svelvet/-/svelvet-8.1.0.tgz",
"integrity": "sha512-rH67tgb7e2aTBQZBCW+V5hSvulLwvzBiOml9Dzdz2ATkgQr58mHi2WtlQFOOb+eZt6zH/J10a2MataC29Qdpuw==",
"dev": true,
"peerDependencies": {
"svelte": ">=3.59.2 || ^4.0.0"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",

View File

@ -10,8 +10,10 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2", "@sveltejs/vite-plugin-svelte": "^2.4.2",
"@xyflow/svelte": "^0.0.12",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-dnd-action": "^0.9.25", "svelte-dnd-action": "^0.9.25",
"svelvet": "^8.1.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vite": "^4.4.5" "vite": "^4.4.5"
}, },

View File

@ -1,10 +1,17 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { inputs, output, filters } from "./stores.js"; import {
import Input from "./Input.svelte"; selectedFilter,
import Output from "./Output.svelte"; addNode,
nodes,
inputs,
output,
filters,
previewCommand,
} from "./stores.js";
import Filter from "./Filter.svelte"; import Filter from "./Filter.svelte";
import FilterPicker from "./FilterPicker.svelte"; import FilterPicker from "./FilterPicker.svelte";
import Graph from "./Graph.svelte";
import { FFmpeg } from "@ffmpeg/ffmpeg"; import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile, toBlobURL } from "@ffmpeg/util"; import { fetchFile, toBlobURL } from "@ffmpeg/util";
import { dndzone } from "svelte-dnd-action"; import { dndzone } from "svelte-dnd-action";
@ -17,7 +24,7 @@
const ffmpeg = new FFmpeg(); const ffmpeg = new FFmpeg();
let command = ""; let command = "";
let videoValue = "/" + $inputs[0]; let videoValue = "/" + $inputs[0].name;
let ffmpegLoaded = false; let ffmpegLoaded = false;
let rendering = false; let rendering = false;
let log = ""; let log = "";
@ -25,7 +32,7 @@
let commandRef; let commandRef;
function newInput() { function newInput() {
$inputs = [...$inputs, "punch.mp4"]; addNode({ name: "punch.mp4" }, "input");
} }
function render() { function render() {
@ -42,15 +49,24 @@
rendering = true; rendering = true;
try { try {
for (let vid of $inputs) { for (let vid of $inputs) {
await ffmpeg.writeFile(vid, await fetchFile("/" + vid)); await ffmpeg.writeFile(vid.name, await fetchFile("/" + vid.name));
} }
const clist = commandList(); let clist = $previewCommand
.replaceAll('"', "")
.replace("ffmpeg", "")
.split(" ")
.filter((i) => i.trim() != "");
console.log("command", clist);
// command.push("-pix_fmt");
// command.push("yuv420p");
// command.push("out.mp4");
await ffmpeg.exec(clist, TIMEOUT); await ffmpeg.exec(clist, TIMEOUT);
// await ffmpeg.exec(["-f", "lavfi", "-i", "color=size=1280x720:rate=25:color=red", "-t", "5", "out.mp4"]) // await ffmpeg.exec(["-f", "lavfi", "-i", "color=size=1280x720:rate=25:color=red", "-t", "5", "out.mp4"])
const data = await ffmpeg.readFile("out.mp4"); const data = await ffmpeg.readFile("out.mp4");
rendering = false; rendering = false;
videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" })); videoValue = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
} catch (e) { } catch (e) {
console.log(e);
log += "Failed"; log += "Failed";
} }
rendering = false; rendering = false;
@ -58,7 +74,6 @@
async function loadFFmpeg() { async function loadFFmpeg() {
ffmpeg.on("log", ({ message: msg }) => { ffmpeg.on("log", ({ message: msg }) => {
// console.log(msg);
log += msg + "\n"; log += msg + "\n";
logbox.scrollTop = logbox.scrollHeight; logbox.scrollTop = logbox.scrollHeight;
}); });
@ -78,68 +93,13 @@
ffmpegLoaded = true; ffmpegLoaded = true;
} }
function updateCommand() {
const cInputs = $inputs.map((i) => `-i ${i}`).join(" ");
const cOutput = $output;
const cFilters = $filters.map(makeFilterArgs).join(",");
let out = `ffmpeg ${cInputs}`;
if (cFilters) out += ` -filter_complex "${cFilters}"`;
out += ` ${cOutput}`;
command = out;
return out;
}
function makeFilterArgs(f) {
let fCommand = f.name;
if (f.params && f.params.length > 0) {
let params = f.params
.map((p) => {
if (p.value === "" || p.value === null || p.value === p.default) return null;
return `${p.name}=${p.value}`;
})
.filter((p) => p !== null)
.join(":");
if (params) fCommand += "=" + params;
}
return fCommand;
}
function commandList() {
let command = [];
for (let vid of $inputs) {
command.push("-i");
command.push(vid);
}
// const audioFilters = $filters.filter(f => f.type[0] === "A").map(makeFilterArgs);
// const videoFilters = $filters.filter(f => f.type[0] === "V").map(makeFilterArgs);
const cFilters = $filters.map(makeFilterArgs).join(",");
if (cFilters.length > 0) {
command.push("-filter_complex");
command.push(cFilters);
}
command.push("-pix_fmt");
command.push("yuv420p");
command.push("out.mp4");
return command;
}
function handleFilterSort(e) { function handleFilterSort(e) {
filters.set(e.detail.items); filters.set(e.detail.items);
} }
inputs.subscribe(updateCommand); // inputs.subscribe(updateCommand);
output.subscribe(updateCommand); // output.subscribe(updateCommand);
filters.subscribe(updateCommand); // filters.subscribe(updateCommand);
onMount(async () => { onMount(async () => {
loadFFmpeg(); loadFFmpeg();
@ -149,49 +109,49 @@
<main> <main>
<section class="header"> <section class="header">
<h1>FFmpeg Explorer</h1> <h1>FFmpeg Explorer</h1>
<div class="help">
<p> <p>
A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a> A tool to help you explore <a href="https://www.ffmpeg.org/" target="_blank">FFmpeg</a>
filters and options. To use: select one or more input videos (there are currently two options), filters. To use:
export and add some filters, and then hit "render" to preview the output in browser. Note: this
is a work in progress, many things may still be broken! Only audio to audio and video to video
filters are included. If it hangs/crashes refresh the page. Post issues/feedback to
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank"
>GitHub</a
>. By <a href="https://lav.io" target="_blank">Sam Lavigne</a>.
</p> </p>
<ol>
<li>Add filters from the list on the left.</li>
<li>Click on filters in the center panel to edit options.</li>
<li>Hit "render" to preview the output in browser.</li>
<li>For more complex filtergraphs, disable "automatic layout."</li>
</ol>
<p>
Note: this is a work in progress, many things may still be broken! If it hangs/crashes
refresh the page. Post issues/feedback to
<a href="https://github.com/antiboredom/ffmpeg-explorer/" target="_blank">GitHub</a>. By
<a href="https://lav.io" target="_blank">Sam Lavigne</a>.
</p>
</div>
</section> </section>
<!-- {message} --> <!-- {message} -->
<section class="command"> <section class="command">
<h3>Output Command</h3> <h3>Output Command</h3>
<div class="inner-command"> <div class="inner-command">
<textarea readonly class="actual-command" bind:this={commandRef}>{command}</textarea> <textarea readonly class="actual-command" bind:this={commandRef} on:click={() => commandRef.select()}>{$previewCommand}</textarea>
<div> <div>
<button on:click={copyCommand}>Copy Command</button> <button on:click={copyCommand}>Copy Command</button>
</div> </div>
</div> </div>
</section> </section>
<section class="inputs">
<div class="section-header">
<h3>Inputs</h3>
<button on:click={newInput}>Add Input</button>
</div>
{#each $inputs as inp, index}
<Input bind:filename={inp} {index} />
{/each}
</section>
<section class="log"> <section class="log">
<h3>FFmpeg Log</h3> <h3>FFmpeg Log</h3>
<textarea readonly class="the-log" bind:this={logbox}>{log}</textarea> <textarea readonly class="the-log" bind:this={logbox}>{log}</textarea>
</section> </section>
<section class="preview"> <section class="preview">
<div class="vid-holder">
{#if rendering} {#if rendering}
<div class="rendering-video"><span>Rendering...</span></div> <div class="rendering-video"><span>Rendering...</span></div>
{/if} {/if}
<video controls src={videoValue} /> <video controls src={videoValue} />
<div style="text-align: right;margin-top:5px;"> </div>
<div style="text-align: right;padding-top:5px;">
<button on:click={render} disabled={!ffmpegLoaded || rendering}> <button on:click={render} disabled={!ffmpegLoaded || rendering}>
{#if ffmpegLoaded} {#if ffmpegLoaded}
{#if rendering} {#if rendering}
@ -206,44 +166,39 @@
</div> </div>
</section> </section>
<section class="output">
<h3>Output</h3>
<Output bind:filename={$output} />
</section>
<section class="filters"> <section class="filters">
<h3>Filters (click to add)</h3> <h3>Filters (click to add)</h3>
<div class="inner-filters">
<div class="filter-picker"> <div class="filter-picker">
<FilterPicker select={"video"} /> <FilterPicker select={"video"} />
</div> </div>
<div </section>
class="filters-holder"
use:dndzone={{ items: $filters }} <section class="graph">
on:consider={handleFilterSort} <Graph />
on:finalize={handleFilterSort} </section>
>
{#each $filters as f (f.id)} <section class="filter-editor">
<div class="filter"> {#if $selectedFilter && $nodes.length > 0 && $nodes[$selectedFilter]}
<Filter bind:filter={f} /> <Filter bind:filter={$nodes[$selectedFilter].data} />
</div> {/if}
{/each}
</div>
</div>
</section> </section>
</main> </main>
<style> <style>
main { main {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: 300px 1fr 1fr 1fr 1fr 300px;
grid-template-areas: grid-template-areas:
"hdr cmd cmd" "hdr log log log prv prv"
"inp log prv" "hdr cmd cmd cmd prv prv"
"out log prv" "flt gra gra gra gra edt";
"flt flt flt"; /* grid-template-rows: 16% 17% 77%; */
grid-template-rows: 15% 15% calc(70% - 40px);
padding: 10px;
grid-gap: 20px; grid-gap: 20px;
padding: 20px; height: 100vh;
border: 1px solid blue;
align-items: stretch;
} }
section { section {
@ -257,6 +212,7 @@
.header { .header {
grid-area: hdr; grid-area: hdr;
overflow: scroll;
} }
.command { .command {
@ -265,17 +221,26 @@
flex-direction: column; flex-direction: column;
} }
.inputs {
grid-area: inp;
}
.preview { .preview {
grid-area: prv; grid-area: prv;
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
} }
.output { .preview video {
grid-area: out; width: 100%;
/* object-fit: contain; */
flex: 1;
}
.vid-holder {
flex: 1;
display: flex;
width: 100%;
height: calc(100% - 30px);
} }
.log { .log {
@ -284,19 +249,25 @@
flex-direction: column; flex-direction: column;
} }
.inner-filters { .filters {
grid-area: flt;
display: flex;
flex-direction: column;
}
.graph {
grid-area: gra;
display: flex; display: flex;
} }
.filters { .filter-editor {
grid-area: flt; grid-area: edt;
display: flex;
} }
.filter-picker { .filter-picker {
max-height: 500px; /* max-height: 500px; */
width: 400px; /* width: 400px; */
position: sticky;
top: 0; top: 0;
left: 0; left: 0;
overflow: scroll; overflow: scroll;
@ -331,14 +302,20 @@
.inner-command { .inner-command {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px 0px;
flex: 1; flex: 1;
} }
textarea {
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
resize: none;
}
.actual-command { .actual-command {
border: none; border: none;
margin-right: 10px; margin-right: 10px;
resize: none;
flex: 1; flex: 1;
font: inherit; font: inherit;
padding: 5px; padding: 5px;
@ -353,6 +330,7 @@
.the-log { .the-log {
border: none; border: none;
resize: none; resize: none;
padding: 5px;
flex: 1; flex: 1;
} }
.rendering-video { .rendering-video {
@ -368,6 +346,17 @@
justify-content: center; justify-content: center;
} }
.help {
font-size: 0.9em;
}
ol {
margin: 5px 0px;
padding-left: 20px;
}
ol li {
margin-bottom: 5px;
}
@media only screen and (max-width: 1400px) { @media only screen and (max-width: 1400px) {
.filters-holder { .filters-holder {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -397,9 +386,6 @@
padding: 10px; padding: 10px;
box-shadow: 2px 2px 0px #000; box-shadow: 2px 2px 0px #000;
} }
.inner-filters {
display: block;
}
.filter-picker { .filter-picker {
width: 100%; width: 100%;
margin-bottom: 20px; margin-bottom: 20px;

View File

@ -1,5 +1,5 @@
<script> <script>
import { filters } from "./stores.js"; import { filters, removeNode } from "./stores.js";
export let filter = { export let filter = {
name: "", name: "",
@ -7,12 +7,10 @@
description: "", description: "",
}; };
let show = false; let show = true;
function remove() { function remove() {
const index = $filters.findIndex((f) => f.id === filter.id); removeNode(filter.id);
$filters.splice(index, 1);
$filters = $filters;
} }
function reset() { function reset() {
@ -29,12 +27,6 @@
<div class="filter-holder"> <div class="filter-holder">
<div class="head"> <div class="head">
<div class="name"><h3>{filter.name}</h3></div> <div class="name"><h3>{filter.name}</h3></div>
<div>
{#if filter.params && filter.params.length > 0}
<button on:click={() => (show = !show)}>{show ? "Hide" : "Show"} Options</button>
{/if}
<button on:click={remove}>X</button>
</div>
</div> </div>
<div class="description"> <div class="description">
@ -95,6 +87,8 @@
/* border: 1px solid #999; */ /* border: 1px solid #999; */
border: 1px solid var(--b1); border: 1px solid var(--b1);
/* box-shadow: 5px 5px 0px #000; */ /* box-shadow: 5px 5px 0px #000; */
overflow-y: scroll;
flex: 1;
} }
.filter-holder, .filter-holder,
input, input,
@ -125,6 +119,10 @@
} }
.p-value { .p-value {
display: flex; display: flex;
justify-content: stretch;
}
input {
width: 100%;
} }
input[type="range"] { input[type="range"] {
margin-right: 5px; margin-right: 5px;

0
src/FilterEditor Normal file
View File

View File

@ -1,8 +1,7 @@
<script> <script>
import uFuzzy from "@leeoniya/ufuzzy"; import uFuzzy from "@leeoniya/ufuzzy";
import { v4 as uuidv4 } from "uuid";
import FILTERS from "./filters.json"; import FILTERS from "./filters.json";
import { filters } from "./stores.js"; import { addNode } from "./stores.js";
export let select = "video"; export let select = "video";
$: selectedFilters = selectFilters(select); $: selectedFilters = selectFilters(select);
@ -26,15 +25,7 @@
} }
function add(f) { function add(f) {
const newFilter = { ...f, filterId: f.id, id: uuidv4() }; addNode(f, "filter");
if (f.params) {
newFilter.params = f.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
}
$filters = [...$filters, newFilter];
} }
function update() { function update() {
@ -56,7 +47,7 @@
<div class="holder"> <div class="holder">
<div class="search"> <div class="search">
<input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" /> <input placeholder="Search Filters" on:keyup={update} bind:value={q} type="text" /><button on:click={() => {reset(); update();}}>X</button>
<select on:change={reset} bind:value={select}> <select on:change={reset} bind:value={select}>
<option value="video">Video Filters</option> <option value="video">Video Filters</option>
<option value="audio">Audio Filters</option> <option value="audio">Audio Filters</option>
@ -82,10 +73,14 @@
} }
.search { .search {
display: flex; display: flex;
justify-items: stretch; justify-content: stretch;
} }
input { input {
width: 100%;
flex: 1; flex: 1;
}
button {
margin-left: 1px;
margin-right: 10px; margin-right: 10px;
} }
.type { .type {

12
src/FitComp.svelte Normal file
View File

@ -0,0 +1,12 @@
<script>
import { useSvelteFlow } from "@xyflow/svelte";
import {nodes, auto} from './stores.js';
const { zoomIn, zoomOut, setZoom, fitView, setCenter, setViewport, getViewport, viewport } =
useSvelteFlow();
nodes.subscribe((n) => {
if ($auto) fitView();
});
</script>

76
src/Graph.svelte Normal file
View File

@ -0,0 +1,76 @@
<script>
import { addNode, nodes, edges, auto, selectedFilter } from "./stores.js";
import {
SvelteFlowProvider,
SvelteFlow,
Controls,
Background,
BackgroundVariant,
} from "@xyflow/svelte";
import Node from "./nodes/Node.svelte";
import FitComp from "./FitComp.svelte";
const nodeTypes = {
ffmpeg: Node,
};
function onClick(e) {
if (e.detail.nodeType === "filter") {
const newSelected = $nodes.findIndex((n) => n.id === e.detail.id);
if (newSelected > -1) {
$selectedFilter = newSelected;
}
}
}
function addInput() {
addNode({ name: "shoe.mp4" }, "input");
}
</script>
<SvelteFlowProvider>
<div class="holder">
<FitComp />
<div class="nav">
<button on:click={addInput}>Add Input</button>
<label for="auto"
><input id="auto" type="checkbox" bind:checked={$auto} />Automatic Layout</label
>
</div>
<div class="flow">
<div style="height: 100%; width: 100%">
<SvelteFlow
nodesConnectable={!auto}
panOnDrag={!auto}
edgesUpdatable={!auto}
connectOnClick={true}
nodesFocusable={!auto}
edgesFocusable={!auto}
on:nodeclick={onClick}
{nodeTypes}
{nodes}
{edges}
snapGrid={[10, 10]}
fitView
>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
</div>
</div>
</div>
</SvelteFlowProvider>
<style>
.holder {
flex-direction: column;
display: flex;
flex: 1;
}
.flow {
flex: 1;
margin-top: 10px;
}
</style>

138
src/GraphOld.svelte Normal file
View File

@ -0,0 +1,138 @@
<script>
import { v4 as uuidv4 } from "uuid";
import { nodes, edges } from "./stores.js";
import { Anchor, Node, Svelvet} from "svelvet";
function onConnect(e) {
console.log(e);
const sourceAnchor = e.detail.sourceAnchor;
const targetAnchor = e.detail.targetAnchor;
const sourceNode = e.detail.sourceNode;
const targetNode = e.detail.targetNode;
// console.log(e);
// console.log(sourceNode.id, "->", targetNode.id)
// console.log(sourceAnchor.id, "->", targetAnchor.id)
edges.update((eds) => {
eds.push({
id: uuidv4(),
sourceAnchor: sourceAnchor.id,
targetAnchor: targetAnchor.id,
source: sourceNode.id,
target: targetNode.id,
});
return eds;
});
}
function onDisconnect(e) {
console.log('dis', e);
const sourceAnchor = e.detail.sourceAnchor.id;
const targetAnchor = e.detail.targetAnchor.id;
const source = e.detail.sourceNode.id;
const target = e.detail.targetNode.id;
// console.log(sourceNode.id, "-/>", targetNode.id)
// console.log(sourceAnchor.id, "-/>", targetAnchor.id)
edges.update((eds) => {
const index = eds.findIndex(
(e) => e.sourceAnchor === sourceAnchor && e.targetAnchor === targetAnchor
);
eds.splice(index, 1);
return eds;
});
}
</script>
<Svelvet
id="my-canvas"
width={800}
height={500}
snapTo={25}
on:disconnection={onDisconnect}
on:connection={onConnect}
>
{#each $nodes as n, index}
<Node
inputs={n.data.inputs.length}
outputs={n.data.outputs.length}
id={n.id}
bind:position={n.position}
>
<div class="node">
<div class="header">
{n.data.name}
</div>
<slot />
</div>
<div class="input-anchors">
{#each n.data.inputs as inp, index}
<Anchor id={inp + "_" + index} let:linked let:connecting let:hovering input>
<div class:linked class:hovering class:connecting class="anchor in {inp}">{inp}</div>
</Anchor>
{/each}
</div>
<div class="output-anchors">
{#each n.data.outputs as out}
<Anchor id={out + "_" + index} let:linked let:connecting let:hovering output>
<div class:linked class:hovering class:connecting class="anchor in {out}">{out}</div>
</Anchor>
{/each}
</div>
</Node>
{/each}
</Svelvet>
<style>
.node {
box-sizing: border-box;
width: fit-content;
height: fit-content;
position: relative;
pointer-events: auto;
display: flex;
flex-direction: column;
padding: 10px;
gap: 10px;
background-color: #fff;
border: 1px solid var(--b1);
font: 12px Times, serif;
box-shadow: none !important;
}
.output-anchors {
position: absolute;
right: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.input-anchors {
position: absolute;
left: -16px;
top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
}
.anchor {
background-color: #fff;
font-size: 12px;
text-align: center;
line-height: 12px;
width: 14px;
height: 14px;
font-family: Times, serif;
border: solid 1px white;
border-color: var(--b1);
}
.hovering {
scale: 1.2;
}
.linked {
background-color: rgb(17, 214, 17) !important;
}
.connecting {
background-color: goldenrod;
}
</style>

View File

@ -1,11 +1,11 @@
<script> <script>
import { inputs } from "./stores.js"; import { removeNode } from "./stores.js";
export let filename = "punch.mp4"; export let filename = "punch.mp4";
export let id;
export let index; export let index;
function remove() { function remove() {
$inputs.splice(index, 1); removeNode(id);
$inputs = $inputs;
} }
</script> </script>

View File

@ -24,6 +24,11 @@ body {
font: font:
16px Times, 16px Times,
serif; serif;
margin: 0;
}
input {
outline: none;
} }
textrea, textrea,
select, select,
@ -32,7 +37,9 @@ button {
font: inherit; font: inherit;
} }
button, input:not([type="range"]) , select { button,
input:not([type="range"]),
select {
border: 1px solid var(--b1); border: 1px solid var(--b1);
background-color: white; background-color: white;
box-shadow: 2px 2px 0px var(--b2); box-shadow: 2px 2px 0px var(--b2);
@ -43,9 +50,7 @@ button:active {
left: 2px; left: 2px;
box-shadow: none; box-shadow: none;
} }
video {
width: 100%;
}
input[type="range"] { input[type="range"] {
-webkit-appearance: none; -webkit-appearance: none;
@ -104,3 +109,11 @@ input[type="range"]::-moz-range-thumb {
input[type="range"]:focus::-moz-range-thumb { input[type="range"]:focus::-moz-range-thumb {
} }
.svelte-flow__node {
border-radius: 0px !important;
border: 1px solid var(--b1) !important;
}
.svelte-flow__attribution {
display: none !important;
}

View File

View File

96
src/nodes/Node.svelte Normal file
View File

@ -0,0 +1,96 @@
<script lang="ts">
import { Handle, Position } from "@xyflow/svelte";
import { removeNode } from "../stores.js";
export let data = { nodeType: "", name: "", inputs: [], outputs: [] };
export let id;
function remove() {
removeNode(id);
}
</script>
<div class="node {data.nodeType}">
<div class="head">
<div class="node-type">{data.nodeType}</div>
<button on:click={remove}>X</button>
</div>
<div class="body">
{#if data.nodeType == "input"}
<select bind:value={data.name}>
<option value="punch.mp4">punch.mp4</option>
<option value="shoe.mp4">shoe.mp4</option>
</select>
{:else}
{data.name}
{/if}
</div>
</div>
{#each data.inputs as inp, index}
<Handle
type="target"
position={Position.Left}
id={inp + "_" + index}
class="handle {inp}"
style="top: {index * 12 + 4}px; left: -7px;">{inp}</Handle
>
{/each}
{#each data.outputs as out, index}
<Handle
type="source"
id={out + "_" + index}
position={Position.Right}
class="handle {out}"
style="top: {index * 12 + 4}px; left: 107%;">{out}</Handle
>
{/each}
<style>
:global(:root) {
--edge-color: var(--b1) !important;
--edge-color-selected: black;
}
:global(.svelte-flow__node) {
box-shadow: 2px 2px 0px var(--b2);
}
:global(.svelte-flow__node.selected) {
outline: 1px solid var(--b1) !important;
}
:global(.handle) {
width: 10px !important;
height: 10px !important;
border: 1px solid var(--b1) !important;
border-radius: 0px !important;
background-color: white !important;
font-size: 10px;
line-height: 6px;
}
:global(.handle.a) {
/* border: 1px solid var(--b2) !important; */
}
.node {
padding: 5px;
}
.head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.head button {
font-size: 10px;
line-height: 8px;
padding: 2px 2px;
margin-left: 10px;
}
.node-type {
font-size: 0.8em;
color: #999;
}
.node.input {
}
.node.filter {
}
.node.output {
}
</style>

View File

View File

@ -1,5 +1,407 @@
import { writable } from 'svelte/store'; import { v4 as uuidv4 } from "uuid";
import { writable, derived, get } from "svelte/store";
export const inputs = writable(["punch.mp4"]); // export const inputs = writable([]);
export const output = writable("out.mp4"); // export const output = writable("out.mp4");
export const filters = writable([]); // export const filters = writable([]);
export const nodes = writable([]);
export const edges = writable([]);
export const auto = writable(true);
export const selectedFilter = writable();
const PREFIX = "";
addNode({ name: "punch.mp4" }, "input");
addNode({ name: "out.mp4" }, "output");
function makeFilterArgs(f) {
let fCommand = f.name;
if (f.params && f.params.length > 0) {
let params = f.params
.map((p) => {
if (p.value === "" || p.value === null || p.value === p.default) return null;
return `${p.name}=${p.value}`;
})
.filter((p) => p !== null)
.join(":");
if (params) fCommand += "=" + params;
}
return fCommand;
}
export const previewCommandSvelvet = derived([edges, nodes], ([$edges, $nodes]) => {
// [0:v]f1=val,f2=val[out] -map out
// [0:v]f1=val,f2=val[1];[1][1:v]overlay[out] -map out`
let finalCommand = [];
let filtergraph = [];
const inputs = $nodes.filter((n) => n.nodeType == "input");
const outputs = $nodes.filter((n) => n.nodeType == "output");
const inputIds = {};
for (let i = 0; i < inputs.length; i++) {
const inp = inputs[i];
inputIds[inp.id] = i;
}
const edgeIds = {};
for (let i = 0; i < $edges.length; i++) {
const e = $edges[i];
edgeIds[e.id] = i + 1;
const source = $nodes.find((n) => PREFIX + n.id === e.source);
const target = $nodes.find((n) => PREFIX + n.id === e.target);
if (!source || !target) continue;
if (source.nodeType === "input") {
if (e.sourceAnchor.startsWith("A-v")) {
edgeIds[e.id] = inputIds[source.id] + ":v";
}
if (e.sourceAnchor.startsWith("A-a")) {
edgeIds[e.id] = inputIds[source.id] + ":a";
}
}
if (target.nodeType === "output") {
edgeIds[e.id] = "out";
}
}
for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
let cmd = "";
const outs = $edges.filter((e) => e.source == PREFIX + n.id);
const ins = $edges.filter((e) => e.target == PREFIX + n.id);
if (outs.length == 0 && ins.length == 0) continue;
for (let i of ins) {
const eid = edgeIds[i.id];
cmd += `[${eid}]`;
}
cmd += makeFilterArgs(n.data);
for (let o of outs) {
const eid = edgeIds[o.id];
cmd += `[${eid}]`;
}
filtergraph.push(cmd);
}
finalCommand.push("ffmpeg");
for (let inp of inputs) {
finalCommand.push("-i");
finalCommand.push(inp.data.name);
}
finalCommand.push("-filter_complex");
finalCommand.push('"' + filtergraph.join(";") + '"');
for (let out of outputs) {
finalCommand.push("-map");
finalCommand.push('"[out]"');
}
for (let inp of inputs) {
finalCommand.push("-map");
finalCommand.push(inputIds[inp.id] + ":a");
}
for (let out of outputs) {
finalCommand.push(out.data.name);
}
const entireCommand = finalCommand.join(" ");
return entireCommand;
});
export const previewCommand = derived([edges, nodes], ([$edges, $nodes]) => {
// [0:v]f1=val,f2=val[out] -map out
// [0:v]f1=val,f2=val[1];[1][1:v]overlay[out] -map out`
let hasVid = false;
let hasAud = false;
let finalCommand = [];
let filtergraph = [];
const inputs = $nodes.filter((n) => n.nodeType == "input");
const outputs = $nodes.filter((n) => n.nodeType == "output");
const inputIds = {};
for (let i = 0; i < inputs.length; i++) {
const inp = inputs[i];
inputIds[inp.id] = i;
}
const edgeIds = {};
for (let i = 0; i < $edges.length; i++) {
const e = $edges[i];
edgeIds[e.id] = i + 1;
const source = $nodes.find((n) => n.id === e.source);
const target = $nodes.find((n) => n.id === e.target);
if (source && target) {
if (source.nodeType === "input") {
if (e.sourceHandle.includes("v")) {
edgeIds[e.id] = inputIds[source.id] + ":v";
}
if (e.sourceHandle.includes("a")) {
edgeIds[e.id] = inputIds[source.id] + ":a";
}
}
if (target.nodeType === "output") {
const outType = e.targetHandle.includes("a") ? "aud_out" : "vid_out";
edgeIds[e.id] = outType;
}
}
}
for (let n of $nodes.filter((n) => n.nodeType == "filter")) {
let cmd = "";
const outs = $edges.filter((e) => e.source == n.id);
const ins = $edges.filter((e) => e.target == n.id);
if (outs.length == 0 && ins.length == 0) continue;
for (let i of ins) {
const eid = edgeIds[i.id];
cmd += `[${eid}]`;
}
cmd += makeFilterArgs(n.data);
for (let o of outs) {
const eid = edgeIds[o.id];
cmd += `[${eid}]`;
}
filtergraph.push(cmd);
}
finalCommand.push("ffmpeg");
for (let inp of inputs) {
finalCommand.push("-i");
finalCommand.push(inp.data.name);
}
if (filtergraph.length > 0) {
const fg = '"' + filtergraph.join(";") + '"';
hasVid = fg.includes(":v]");
hasAud = fg.includes(":a]");
finalCommand.push("-filter_complex");
finalCommand.push(fg);
finalCommand.push("-map");
if (hasAud) {
finalCommand.push('"[aud_out]"');
} else {
finalCommand.push("0:a");
}
if (hasVid) {
finalCommand.push("-map");
finalCommand.push('"[vid_out]"');
}
}
// for (let inp of inputs) {
// finalCommand.push("-map");
// finalCommand.push(inputIds[inp.id] + ":a");
// }
for (let out of outputs) {
finalCommand.push(out.data.name);
}
const entireCommand = finalCommand.join(" ");
return entireCommand;
});
export const previewCommandOld = derived(nodes, ($nodes) => {
const cInputs = $nodes
.filter((n) => n.nodeType === "input")
.map((i) => `-i ${i.data.name}`)
.join(" ");
const cOutput = $nodes
.filter((n) => n.nodeType === "output")
.map((i) => `${i.data.name}`)
.join(" ");
const cFilters = $nodes
.filter((n) => n.nodeType === "filter")
.map((n) => n.data)
.map(makeFilterArgs)
.join(",");
let out = `ffmpeg ${cInputs}`;
if (cFilters) out += ` -filter_complex "${cFilters}"`;
out += ` ${cOutput}`;
return out;
});
export const inputs = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "input").map((n) => n.data);
});
export const filters = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "filter").map((n) => n.data);
});
export const output = derived(nodes, ($nodes) => {
return $nodes.filter((n) => n.nodeType === "output").map((n) => n.data);
});
nodes.subscribe(($nodes) => {
const isAuto = get(auto);
if (!isAuto) return;
const outputNodes = $nodes.filter((n) => n.nodeType === "output");
const inputNodes = $nodes.filter((n) => n.nodeType === "input");
const filterNodes = $nodes.filter((n) => n.nodeType === "filter");
const orderedNodes = [].concat(filterNodes, outputNodes).filter((n) => n != undefined);
const filled = [];
let newEdges = [];
function connectNode(n1, rest) {
for (let i = 0; i < n1.data.outputs.length; i++) {
const edgeType = n1.data.outputs[i];
for (let j = 0; j < rest.length; j++) {
let found = false;
const n2 = rest[j];
for (let k = 0; k < n2.data.inputs.length; k++) {
const targetEdgeType = n2.data.inputs[k];
if (edgeType === targetEdgeType && !filled.includes(n2.id + k)) {
newEdges.push({
id: uuidv4(),
type: "default",
source: n1.id,
target: n2.id,
sourceHandle: edgeType + "_" + i,
targetHandle: edgeType + "_" + k,
});
filled.push(n2.id + k);
found = true;
break;
}
}
if (found) break;
}
}
const nextNode = rest.shift();
if (nextNode) {
connectNode(nextNode, rest);
}
}
for (let inpNode of inputNodes) {
connectNode(inpNode, [...orderedNodes]);
}
// console.log("new", newEdges);
edges.set(newEdges);
});
export function addNode(data, type) {
let ins = [];
let outs = [];
if (type === "input") {
outs = ["v", "a"];
} else if (type === "output") {
ins = ["v", "a"];
} else if (type === "filter") {
const [_ins, _outs] = data.type.split("->");
ins = _ins.toLowerCase().split("");
outs = _outs.toLowerCase().split("");
if (data.params) {
data.params = data.params.map((p) => {
p.value = null;
if (p.default != null) p.value = p.default;
return p;
});
}
}
data.nodeType = type;
data.inputs = ins;
data.outputs = outs;
let node = {
id: uuidv4(),
type: "ffmpeg",
data: data,
nodeType: type,
position: { x: 0, y: 0 },
};
nodes.update((_nodes) => {
_nodes.push(node);
const isAuto = get(auto);
if (isAuto) {
const w = 120;
const h = 50;
const margin = 50;
let prev = null;
for (let n of _nodes) {
if (n.nodeType === "input") {
n.position = { x: 0, y: prev ? prev.position.y + h + margin : 0 };
prev = n;
}
}
for (let n of _nodes) {
if (n.nodeType === "filter") {
let _w = prev && prev.width ? prev.width : w;
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: -50 };
prev = n;
}
}
for (let n of _nodes) {
if (n.nodeType === "output") {
let _w = prev && prev.width ? prev.width : w;
n.position = { x: prev ? prev.position.x + _w + margin : 0, y: 0 };
}
}
}
if (node.nodeType === "filter") {
selectedFilter.set(_nodes.length - 1);
}
return _nodes;
});
}
export function removeNode(id) {
nodes.update((_nodes) => {
const index = _nodes.findIndex((n) => n.id === id);
_nodes.splice(index, 1);
return _nodes;
});
edges.update((_edges) => {
for (let i = _edges.length - 1; i--; i >= 0) {
const e = _edges[i];
if ("N-" + e.source === id || "N-" + e.target === id) {
_edges.splice(i, 1);
}
}
return _edges;
});
}