Compare commits

...

45 Commits

Author SHA1 Message Date
Emerald 92b9d8f752
fix threshold arrow alignment 2023-07-12 01:18:44 -04:00
Emerald 4449bacaa1
upgrade bevy_mod_picking 2023-07-11 01:02:50 -04:00
Emerald 54972b7cf7
add icons for level bars 2023-06-20 13:59:40 -04:00
Emerald a22f7f0b8f
add feature to enable wayland 2023-06-20 01:04:10 -04:00
Emerald a8e9f72b49
set and trigger threshold from slider 2023-06-18 21:05:57 -04:00
Emerald 6b5a12c5a7
threshold slider attempt 2023-06-18 14:39:41 -04:00
Emerald 2d03f1b5ff
fix mic level scaling 2023-06-16 01:49:40 -04:00
Emerald 828bf6bb75
basic mic level animation 2023-06-15 14:33:37 -04:00
Emerald 83b3b3d3ec
hide and show ui 2023-06-14 12:47:26 -04:00
Emerald 176eadf6d0
image frames using bevy ui 2023-06-13 12:22:22 -04:00
Emerald 40e996a7e8
initial ray asset support 2023-06-11 02:05:23 -04:00
Emerald ea98b54199
use zip archives for ray format (again) 2023-06-11 00:14:42 -04:00
Emerald 3346ed22fa
react to window focus 2023-06-10 17:31:34 -04:00
Emerald 6f22890d07
fix mouth state buffer 2023-06-10 17:20:18 -04:00
Emerald 09769de7a1
clippy 2023-06-10 14:47:31 -04:00
Emerald d822a1a8c0
basic reactive animation 2023-06-10 14:46:56 -04:00
Emerald 547dad2060
basic scripting infra 2023-06-09 16:32:57 -04:00
Emerald 29d9e6c4e0
reimplement audio stuff 2023-06-09 01:43:51 -04:00
Emerald 2ad75e8e90
start bevy rewrite 2023-06-08 13:59:26 -04:00
Emerald 1557399e34
ray: basic database functions 2023-06-08 01:45:51 -04:00
Emerald 87789399cd
use canvas for main image display 2023-06-06 18:42:19 -04:00
Emerald 6579c6a599
update deps 2023-04-15 16:14:30 -04:00
emerald 97bf38165f
make config saving more consistent 2022-12-17 17:15:02 -05:00
emerald 200f71e0ac
fix blink interval not being set correctly 2022-12-17 15:54:08 -05:00
emerald 36db69356c
limit minimum blink threshold 2022-12-17 15:26:28 -05:00
emerald 91ce120921
add js infrastructure for assigning animatinons to frames 2022-11-12 22:14:35 -05:00
emerald 79a31a161c
improve animation translations 2022-11-11 13:00:36 -05:00
emerald 5f7041ddc2
switch to js animations 2022-11-10 13:31:02 -05:00
emerald a51ae66019
fix config not saving
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-11-10 12:48:15 -05:00
emerald f20b32c986
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
ci/woodpecker/pr/build Pipeline was successful Details
ci/woodpecker/pr/publish Pipeline was successful Details
2022-10-21 02:14:57 -04:00
emerald 31bbf44171
add mic sensitivity to config 2022-10-21 02:03:18 -04:00
emerald 2489210068
add command to save config
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-20 14:03:59 -04:00
emerald c5c587ad3c
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 14:27:23 -04:00
emerald 14a86f4b63
add 'new' button, add image loading icon
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 14:25:42 -04:00
emerald dbafaa77d6
update changelog
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-19 01:32:23 -04:00
emerald 72ef196223
add config loading 2022-10-19 01:30:58 -04:00
emerald 62253bbeb6
add bundle to gitignore 2022-10-18 23:53:57 -04:00
emerald 468cd79692
add install and uninstall recipes
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-18 14:06:53 -04:00
emerald 9e6e688a97 Update 'README.md'
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-17 18:47:34 -04:00
emerald f60fcf0556
let build pipeline build debug version
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-16 21:31:37 -04:00
emerald 1dd2eac1a0
let build pipeline run on dev branch
ci/woodpecker/push/publish unknown status Details
ci/woodpecker/push/build Pipeline was successful Details
2022-10-16 17:46:06 -04:00
emerald 8452a15001
bump version in tauri.conf 2022-10-16 17:42:48 -04:00
emerald 8433a128db
fix rpm deps 2022-10-16 11:13:10 -04:00
emerald 849e437ae5
skip bundling in build pipeline 2022-10-16 01:22:20 -04:00
emerald 990fcfbc79
update frontend deps 2022-10-16 01:17:13 -04:00
88 changed files with 4223 additions and 6202 deletions

8
.gitignore vendored
View File

@ -1,2 +1,10 @@
node_modules/
dist/
bundle/
target/
# Added by cargo
/target

11
.helix/languages.toml Normal file
View File

@ -0,0 +1,11 @@
[[language]]
name = "svelte"
auto-format = true
[[language]]
name = "typescript"
auto-format = true
[[language]]
name = 'toml'
auto-format = true

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
rust-analyzer latest

View File

@ -1,4 +1,4 @@
branches: main
branches: [main, dev]
pipeline:
build:
image: gitea.greenboi.me/emerald/cathode-build:latest
@ -7,4 +7,5 @@ pipeline:
event: [push, pull_request]
commands:
- npm ci
- npm run tauri build
- npm run build
- cd src-tauri && cargo build

View File

@ -6,11 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [v0.1.0]
### Added
- Load last selected ray file at launch
- Add `blink_interval` to config file
- Add `mic_sens` to config file
- Add `new` button to reset the loaded ray
- Add icon to indicate an image is being loaded
- Add shift-click to clear a frame
### Changed
- Loading rays at launch should be more consistent
### Fixed
- Fixed config not being respected on launch

File diff suppressed because it is too large Load Diff

34
Cargo.toml Normal file
View File

@ -0,0 +1,34 @@
[package]
name = "cathode"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = ["ray_format"]
[dependencies]
anyhow = "1.0.71"
bevy = { version = "0.10.1" }
bevy_eventlistener = "0.2.2"
bevy_framepace = "0.12.1"
bevy_mod_picking = { version = "0.14", default-features = false, features = ["backend_bevy_ui", "debug"] }
bevy_mod_scripting = { version = "0.3.0", features = ["rhai"] }
bevy_ninepatch = "0.10.0"
bevy_tweening = "0.7.0"
cpal = { version = "0.15.2" }
directories = "5.0.1"
ray_format = { version = "0.2.0", path = "ray_format", features = ["async_std"] }
rhai = { version = "1.14.0", features = ["sync"] }
[features]
default = ["wayland"]
jack = ["cpal/jack"]
wayland = ["bevy/wayland"]
[profile.dev.package."*"]
opt-level = 3
[profile.dev]
opt-level = 1

View File

@ -1,7 +0,0 @@
FROM node
RUN apt update -yy && apt upgrade -yy
RUN apt install -yy libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
RUN apt install -yy libasound2-dev
RUN curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
RUN mv /root/.cargo/bin/* /usr/bin
ENTRYPOINT ["/bin/bash"]

View File

@ -1,4 +1,40 @@
# Cathode [![status-badge](https://ci.greenboi.me/api/badges/emerald/cathode/status.svg)](https://ci.greenboi.me/emerald/cathode)
a small app for PNG tubing. Think Veadotube-mini but completely FOSS.
Built with Tauri and Svelte.
Built with Tauri and Svelte.
## Installation
### Packages
There are a few prebuilt packages available [here](https://gitea.greenboi.me/emerald/-/packages/generic/cathode-tube/) for the latest stable release. They are built on Bullseye Debian, so should be compatible with most up to date systems. Debian derived distros will want the `.deb` file, and Fedora users will want the `.rpm` file. Download the correct file and install it with your package manager.
Alternatively, download the AppImage, which should work on any glibc linux distro at the cost of being a considerable larger file. Once you download the `.AppImage` file, give it execution permissions (eg: `chmod +x cathode-tube.AppImage`) and run it like a command or script.
### Building from source
#### Prerequisites
In order to build from source you will need a few things to get started
- Nodejs/npm (I recommend using [nvm](https://github.com/nvm-sh/nvm) for this)
- [Rust](https://rustup.rs/)
- Tauri's [development dependencies](https://tauri.app/v1/guides/getting-started/prerequisites#installing)
#### Building
Once all of these are installed, clone the repo and run
```
npm install
```
This will install everything needed to build the frontend, as well as the tauri cli. Building the project itself is then as simple as
```
npm run tauri build
```
This will build the frontend and backend, and bundle the `.deb` and `.AppImage` packages, found in `src-tauri/target/release/bundle`.
The binary itself (at `src-tauri/target/release/cathode-tube`) is all that is needed to run the program, so if neither bundle works for you simply copy the executable to somewhere in your path, or run
```
cargo install --path src-tauri
```
#### Just
If you have the [just](https://github.com/casey/just) command runner installed, as well as the other prerequisites, then you can run
```
just install
```
Which will build the project and install it to `/usr/bin`, along with the `.desktop` file and icons

BIN
assets/lips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

BIN
assets/microphone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

BIN
assets/plain-arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

BIN
assets/plain-square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
assets/rat1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
assets/rat2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,3 @@
pub fn update(transform) {
}

BIN
assets/square-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,7 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -2,19 +2,40 @@ set export
alias d := debug
watch := "cargo watch -q -c -x run"
dev:
-cargo tauri dev
{{watch}}
debug:
RUST_LOG=debug cargo tauri dev
RUST_LOG=debug {{watch}}
log RUST_LOG:
cargo tauri dev
log RUST_LOG: dev
build:
cargo tauri build
cd src-tauri
cargo generate-rpm
cp -r target/release/bundle ..
cp -r target/generate-rpm ../bundle
cargo build
build-release:
cargo build --release
install: build-release
@echo Copying binary to /usr/bin/cathode...
@sudo install target/release/cathode /usr/bin/cathode
@echo Installing desktop file...
@sudo install desktop/cathode-tube.desktop /usr/share/applications/cathode-tube.desktop
@sudo install desktop/application-cathode.xml /usr/share/mime/packages/application-cathode.xml
@echo Installing icons...
@sudo install desktop/icons/128x128.png /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo install desktop/icons/32x32.png /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo install desktop/icons/128x128@2x.png /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database
uninstall:
@echo Removing cathode...
@sudo rm -f /usr/bin/cathode
@sudo rm -f /usr/share/applications/cathode-tube.desktop
@sudo rm -f /usr/share/mime/packages/application-cathode.xml
@sudo rm -f /usr/share/icons/hicolor/128x128/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/32x32/apps/cathode-tube.png
@sudo rm -f /usr/share/icons/hicolor/256x256/apps/cathode-tube.png
@sudo update-desktop-database

2041
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +0,0 @@
{
"name": "cathode-tube",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"tauri": "tauri"
},
"dependencies": {
"@neodrag/svelte": "^1.2.3",
"@tauri-apps/api": "^1.0.2"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tauri-apps/cli": "^1.1.1",
"@tsconfig/svelte": "^3.0.0",
"@types/node": "^18.7.10",
"sass": "^1.54.8",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.2"
}
}

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 330 330" style="enable-background:new 0 0 330 330;" xml:space="preserve">
<g id="XMLID_894_">
<g id="XMLID_895_">
<path id="XMLID_896_" d="M164.998,210c35.888,0,65.085-29.195,65.085-65.12l-0.204-80c0-35.775-29.105-64.88-64.881-64.88
c-35.773,0-64.877,29.105-64.877,64.842l-0.203,80.076C99.918,180.805,129.112,210,164.998,210z M130.121,64.88
c0-19.233,15.646-34.88,34.877-34.88c19.233,0,34.881,15.647,34.881,34.919l0.204,80c0,19.344-15.739,35.081-35.085,35.081
c-19.343,0-35.08-15.737-35.08-35.044L130.121,64.88z"/>
</g>
<g id="XMLID_899_">
<path id="XMLID_900_" d="M280.084,154.96c0-8.284-6.716-15-15-15c-8.284,0-15,6.716-15,15c0,46.732-37.878,84.774-84.546,85.068
c-0.181-0.006-0.357-0.027-0.54-0.027c-0.183,0-0.359,0.021-0.541,0.027c-46.665-0.293-84.541-38.335-84.541-85.068
c0-8.284-6.716-15-15-15s-15,6.716-15,15c0,58.373,43.688,106.731,100.082,114.104V300H117c-8.284,0-15,6.716-15,15
s6.716,15,15,15h96.001c8.284,0,15-6.716,15-15s-6.716-15-15-15h-33.003v-30.936C236.395,261.69,280.084,213.332,280.084,154.96z"
/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +0,0 @@
<svg width="64px" height="64px" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--emojione-monotone" preserveAspectRatio="xMidYMid meet"><path d="M32 2C15.432 2 2 15.432 2 32s13.432 30 30 30s30-13.432 30-30S48.568 2 32 2zm0 57.5C16.836 59.5 4.5 47.164 4.5 32S16.836 4.5 32 4.5S59.5 16.836 59.5 32S47.164 59.5 32 59.5z" fill="currentColor"></path><circle cx="32" cy="45.139" r="7" fill="currentColor"></circle><circle cx="20.248" cy="25.045" r="4.5" fill="currentColor"></circle><circle cx="42.75" cy="25.045" r="4.5" fill="currentColor"></circle></svg>

Before

Width:  |  Height:  |  Size: 654 B

1
ray_format/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=sqlite:test.db

View File

@ -1,6 +1,6 @@
[package]
name = "ray_format"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["AnActualEmerald"]
@ -8,9 +8,20 @@ authors = ["AnActualEmerald"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.62"
log = "0.4.17"
qoi = "0.4.0"
serde = "1.0.144"
serde_json = "1.0.85"
image = "0.24.6"
serde = {version="1.0.144", features = ["derive"]}
serde_json = "1.0.96"
sha256 = "1.1.2"
thiserror = "1.0.40"
tracing = "0.1.37"
uuid = { version = "1.3.3", features = ["v4", "serde"] }
zip = "0.6.2"
[dev-dependencies]
tokio-test = "0.4.2"
tracing-test = { version = "0.2.4"}
[features]
tokio = []
async_std = []

View File

@ -0,0 +1,29 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS images
(
id VARCHAR PRIMARY KEY,
image BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS animations
(
id INTEGER PRIMARY KEY,
name VARCHAR(32) NOT NULL,
animation VARCHAR NOT NULL
);
CREATE TABLE IF NOT EXISTS expressions
(
id INTEGER PRIMARY KEY,
imageId INTEGER NOT NULL REFERENCES images(id),
inAnimation INTEGER REFERENCES animations(id),
outAnimation INTEGER REFERENCES animations(id),
idleAnimation INTEGER REFERENCES animations(id)
);
CREATE TABLE IF NOT EXISTS meta
(
format_version INTEGER PRIMARY KEY
);
INSERT INTO meta VALUES (1) ON CONFLICT DO NOTHING;

View File

@ -1,53 +1,236 @@
use anyhow::Context;
use anyhow::Result;
use std::io;
use std::io::Cursor;
use std::io::Read;
use std::io::Write;
use zip::{write::FileOptions, ZipArchive, ZipWriter};
use crate::error::Error;
use crate::model::Expression;
use crate::model::Frame;
use image::io::Reader as ImageReader;
use uuid::Uuid;
use zip::write::FileOptions;
use zip::{ZipArchive, ZipWriter};
pub(crate) struct Archive {
buffer: Vec<u8>,
#[derive(Debug)]
pub struct Archive<T> {
buffer: T,
}
impl Archive {
pub(crate) fn open(data: &[u8]) -> Self {
Self {
buffer: data.to_vec(),
impl Archive<Vec<u8>> {
pub fn open_buf(mut buffer: Vec<u8>) -> Result<Self, Error> {
ZipWriter::new(Cursor::new(&mut buffer));
let archive = Self { buffer };
Ok(archive)
}
pub fn get_image(&self, id: &str) -> Result<Vec<u8>, Error> {
let mut archive = self.get_reader()?;
let mut img = archive.by_name(format!("images/{}", id).as_str())?;
let mut buf = vec![];
img.read_to_end(&mut buf)?;
Ok(buf)
}
pub fn add_image(&mut self, data: &[u8]) -> Result<String, Error> {
let decoded = ImageReader::new(Cursor::new(data))
.with_guessed_format()?
.decode()?;
// all images are re-encoded to qoi
let mut qoi = vec![];
decoded.write_to(&mut Cursor::new(&mut qoi), image::ImageOutputFormat::Qoi)?;
let id = sha256::digest(qoi.as_slice());
let mut archive = self.get_writer()?;
archive.start_file(format!("images/{id}"), FileOptions::default())?;
archive.write_all(&qoi)?;
archive.finish()?;
Ok(id)
}
pub fn get_frame(&self, id: &str) -> Result<Frame, Error> {
let mut archive = self.get_reader()?;
let file = archive.by_name(format!("frames/{id}.frame").as_str())?;
let raw = file.extra_data();
serde_json::from_slice(raw).map_err(|e| e.into())
}
pub fn get_expression(&self, name: Option<&str>) -> Result<Expression, Error> {
let name = name.unwrap_or("default");
if let Ok(mut file) = self.get_reader()?.by_name(format!("{name}.exp").as_str()) {
let mut buf = vec![];
file.read_to_end(&mut buf)?;
Ok(serde_json::from_slice(&buf)?)
} else {
Ok(Expression::default())
}
}
pub(crate) fn new() -> Self {
let mut buffer = vec![];
ZipWriter::new(Cursor::new(&mut buffer));
Self { buffer }
pub fn show_expressions(&self) -> Result<Vec<String>, Error> {
let archive = self.get_reader()?;
let mut res = vec![];
for name in archive.file_names() {
if name.ends_with(".exp") && !name.contains('/') {
res.push(
name.strip_suffix(".exp")
.ok_or_else(|| {
Error::OtherError("Expected file name to end in '.exp'".into())
})?
.to_string(),
)
}
}
Ok(res)
}
pub(crate) fn buffer(&self) -> &[u8] {
self.buffer.as_ref()
}
pub fn write_frame(&mut self, frame: &Frame) -> Result<(), Error> {
let mut archive = self.get_writer()?;
pub(crate) fn add_file(&mut self, file_name: &str, data: &[u8]) -> Result<()> {
let mut zip = ZipWriter::new_append(Cursor::new(&mut self.buffer))?;
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Bzip2)
.compression_level(Some(9));
zip.start_file(file_name, options)?;
zip.write_all(data)?;
zip.finish()?;
let stringy = serde_json::to_string(frame)?;
archive.start_file(format!("frames/{}.frame", frame.id), FileOptions::default())?;
archive.write_all(stringy.as_bytes())?;
archive.finish()?;
Ok(())
}
pub(crate) fn get_file(&self, file_name: &str) -> Result<Vec<u8>> {
let mut buffer = vec![];
let mut zip = ZipArchive::new(Cursor::new(&self.buffer))?;
pub fn new_frame(
&mut self,
image: &[u8],
in_animation: Option<String>,
out_animation: Option<String>,
idle_animation: Option<String>,
) -> Result<Frame, Error> {
let image_id = self.add_image(image)?;
let id = Uuid::new_v4();
let mut f = zip
.by_name(file_name)
.context("Couldn't find a file by that name")?;
io::copy(&mut f, &mut Cursor::new(&mut buffer))?;
let frame = Frame {
id,
image: image_id,
in_animation,
out_animation,
idle_animation,
};
Ok(buffer)
self.write_frame(&frame)?;
Ok(frame)
}
pub fn write_expression(&mut self, name: Option<&str>, exp: &Expression) -> Result<(), Error> {
let mut archive = self.get_writer()?;
archive.start_file(
format!("{}.exp", name.unwrap_or("default")),
FileOptions::default(),
)?;
archive.write_all(&serde_json::to_vec(exp)?)?;
archive.finish()?;
Ok(())
}
fn get_reader(&self) -> Result<ZipArchive<Cursor<&Vec<u8>>>, Error> {
ZipArchive::new(Cursor::new(&self.buffer)).map_err(|e| e.into())
}
fn get_writer(&mut self) -> Result<ZipWriter<Cursor<&mut Vec<u8>>>, Error> {
ZipWriter::new_append(Cursor::new(&mut self.buffer)).map_err(|e| e.into())
}
}
#[cfg(test)]
mod test {
use std::io::Cursor;
use image::io::Reader as ImageReader;
use image::{codecs::png::PngEncoder, ImageEncoder};
use tracing_test::traced_test;
use crate::model::Expression;
use super::Archive;
#[test]
fn get_default_expression() {
let archive = Archive::open_buf(Vec::with_capacity(1024)).unwrap();
let res = archive.get_expression(None).unwrap();
assert_eq!(res, Expression::default());
}
#[test]
fn add_frame() {
let mut archive = Archive::open_buf(Vec::with_capacity(1024)).unwrap();
let test_data = image::RgbImage::from_fn(256, 256, |x, y| {
if x % 2 == 0 && y % 2 == 0 {
[255, 0, 255].into()
} else {
[255, 0, 255].into()
}
});
let mut buf = Cursor::new(vec![]);
PngEncoder::new(&mut buf)
.write_image(test_data.as_raw(), 256, 256, image::ColorType::Rgb8)
.unwrap();
let exp = archive.new_frame(buf.get_ref(), None, None, None).unwrap();
let raw_image = archive.get_image(&exp.image).unwrap();
let image = ImageReader::new(Cursor::new(raw_image))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
assert_eq!(test_data, image.into_rgb8());
}
#[test]
#[traced_test]
fn add_image() {
let archive = Archive::open_buf(Vec::with_capacity(1024));
assert!(archive.is_ok());
let mut archive = archive.unwrap();
let test_data = image::RgbImage::from_fn(256, 256, |x, y| {
if x % 2 == 0 && y % 2 == 0 {
[255, 0, 255].into()
} else {
[255, 0, 255].into()
}
});
let mut buf = Cursor::new(vec![]);
PngEncoder::new(&mut buf)
.write_image(test_data.as_raw(), 256, 256, image::ColorType::Rgb8)
.unwrap();
let id = archive.add_image(&buf.into_inner());
let res = archive.get_image(&id.unwrap());
let returned_image = ImageReader::new(Cursor::new(res.unwrap()))
.with_guessed_format()
.unwrap()
.decode()
.unwrap();
assert_eq!(test_data, returned_image.into_rgb8());
}
}

20
ray_format/src/error.rs Normal file
View File

@ -0,0 +1,20 @@
use std::string::FromUtf8Error;
use thiserror::Error;
use zip::result::ZipError;
#[derive(Error, Debug)]
pub enum Error {
#[error("Error with image data: {0}")]
ImageError(#[from] image::ImageError),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Error parsing string: {0}")]
Utf8Error(#[from] FromUtf8Error),
#[error("Error reading zip file: {0}")]
ZipError(#[from] ZipError),
#[error("Error parsing JSON: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Unknown error: {0}")]
OtherError(String),
}

View File

@ -1,8 +1,7 @@
#[cfg(test)]
mod test;
mod archive;
mod manage;
mod model;
pub use model::Ray;
pub mod error;
pub use archive::Archive as Ray;
pub use model::*;

View File

@ -1,80 +1,20 @@
use anyhow::Result;
use std::{
collections::{hash_map::Keys, HashMap},
fs,
path::Path,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::archive::Archive;
#[derive(Default, Clone, Debug)]
pub struct Ray {
frames: [Vec<u8>; 4],
extras: HashMap<String, Vec<u8>>,
meta: HashMap<String, String>,
/// An frame with the id of its relevant images and animations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub id: Uuid,
pub image: String,
pub in_animation: Option<String>,
pub out_animation: Option<String>,
pub idle_animation: Option<String>,
}
impl Ray {
pub fn get_meta_keys(&self) -> Keys<String, String> {
self.meta.keys()
}
pub fn get_meta_value(&self, key: &str) -> Option<&String> {
self.meta.get(key)
}
pub fn get_frame(&self, index: usize) -> Option<&Vec<u8>> {
self.frames.get(index)
}
pub fn set_frame(&mut self, index: usize, data: Vec<u8>) -> bool {
if let Some(i) = self.frames.get_mut(index) {
*i = data;
true
} else {
false
}
}
pub fn add_meta(&mut self, key: String, value: String) -> bool {
self.meta.insert(key, value).is_none()
}
pub fn save(&self, path: &Path) -> Result<()> {
let mut archive = Archive::new();
for (i, f) in self.frames.iter().enumerate() {
if f.len() > 0 {
archive.add_file(&format!("{}", i), f)?
}
}
for (n, f) in self.extras.iter() {
archive.add_file(&n, f)?;
}
let meta = serde_json::to_string(&self.meta)?;
archive.add_file("meta.json", meta.as_bytes())?;
fs::write(path, archive.buffer())?;
Ok(())
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let mut ray = Self::default();
let data = fs::read(path.as_ref())?;
let archive = Archive::open(&data);
for i in 0..4 {
if let Ok(buf) = archive.get_file(&i.to_string()) {
ray.set_frame(i as usize, buf);
}
}
if let Ok(buf) = archive.get_file("meta.json") {
let meta: HashMap<String, String> = serde_json::from_slice(&buf)?;
ray.meta = meta;
}
Ok(ray)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Expression {
pub mouth_closed_eyes_open: Option<Uuid>,
pub mouth_open_eyes_open: Option<Uuid>,
pub mouth_closed_eyes_closed: Option<Uuid>,
pub mouth_open_eyes_closed: Option<Uuid>,
}

View File

@ -1,22 +1,26 @@
use std::io::Cursor;
use crate::archive::Archive;
use image::{DynamicImage, ImageBuffer, ImageFormat};
#[test]
fn add_file_to_archive() {
let mut archive = Archive::new();
let res = archive.add_file("hello", b"world");
#[sqlx::test]
async fn add_image() {
let mut archive = Archive::open(":memory:").await.expect("Failed to open db");
let img_buf = DynamicImage::ImageRgb8(ImageBuffer::from_fn(64, 64, |x, _| {
if x % 2 == 0 {
image::Rgb([0u8, 0u8, 0u8])
} else {
image::Rgb([255u8, 255u8, 255u8])
}
}));
let mut buf = vec![];
img_buf
.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)
.unwrap();
let res = archive.add_image(&buf).await;
println!("{:?}", res);
assert!(res.is_ok());
}
#[test]
fn get_file_from_archive() {
let mut archive = Archive::new();
archive.add_file("hello", b"world").unwrap();
let file = archive.get_file("hello");
assert!(file.is_ok());
assert_eq!(
String::from("world"),
String::from_utf8(file.unwrap()).unwrap()
);
}

BIN
ray_format/test.db-shm Normal file

Binary file not shown.

BIN
ray_format/test.db-wal Normal file

Binary file not shown.

View File

@ -1,4 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

@ -1,54 +0,0 @@
[package]
name = "cathode-tube"
version = "0.1.0"
description = "A Tauri App"
authors = ["AnActualEmerald"]
license = "GPL-3.0-or-later"
repository = "https://github.com/AnActualEmerald/cathode"
edition = "2021"
[package.metadata.generate-rpm]
assets = [
{source= "target/release/cathode-tube", dest= "/usr/bin/cathode", mode= "755"},
{source="cathode-tube.desktop", dest="/usr/share/applications/cathode-tube.desktop", mode="0644"},
{source="application-cathode.xml", dest="/usr/share/mime/packages/application-cathode.xml", mode="0644"},
{source="icons/128x128.png", dest="/usr/share/icons/hicolor/128x128/apps/cathode-tube.png", mode="0644"},
{source="icons/128x128@2x.png", dest="/usr/share/icons/hicolor/256x256@2/apps/cathode-tube.png", mode="0644"},
]
[workspace]
[profile.release]
strip = true
opt-level = "s"
lto = true
codegen-units = 1
[build-dependencies]
tauri-build = { version = "1.1.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.1.1", features = ["cli", "dialog-all", "fs-create-dir", "fs-read-dir", "fs-read-file", "fs-write-file", "macos-private-api", "window-minimize", "window-set-max-size", "window-set-min-size", "window-unminimize"] }
cpal = { version = "0.14.0", features = ["jack"] }
ray_format = {path = "../ray_format", version = "~0.1.0"}
anyhow = "1.0.65"
log = "0.4.17"
env_logger = "0.9.1"
rand = "0.8.5"
tokio = { version = "1.21.2", features = ["full"] }
image = "0.24.4"
base64-url = "1.4.13"
toml = "0.5.9"
notify = "5.0.0"
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/cathode">
<comment>Cathode RAY file</comment>
<sub-class-of type="application/zip" />
<icon name="application-cathode" />
<acronym>RAY</acronym>
<glob pattern="*.ray"/>
</mime-type>
</mime-info>

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Icon=cathode-tube
Name=Cathode
Exec=cathode-tube %U
Terminal=false
Hidden=false
Categories=Graphics; Video
Comment=Small app for PNG-tubing
MimeType=application/cathode

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,54 +0,0 @@
use std::sync::Arc;
use std::sync::Mutex;
use std::thread::sleep;
use std::time::Duration;
use anyhow::anyhow;
use anyhow::Result;
use cpal::traits::StreamTrait;
use cpal::traits::{DeviceTrait, HostTrait};
use cpal::Device;
use cpal::InputCallbackInfo;
use log::debug;
use tauri::Window;
pub async fn monitor(window: Window, threshold: Arc<Mutex<f32>>, level: Arc<Mutex<f32>>) {
let device = initialize().expect("Unable to init audio");
debug!("Using device {}", device.name().unwrap());
let config = device.default_input_config().unwrap();
let stream = device
.build_input_stream(
&config.config(),
move |data: &[f32], _: &InputCallbackInfo| {
if data.iter().any(|e| e.abs() >= *threshold.lock().unwrap()) {
window.emit("mouth-open", "").unwrap();
} else {
window.emit("mouth-close", "").unwrap();
}
*level.lock().unwrap() = data
.iter()
.map(|e| e.abs())
.max_by(|a, b| a.total_cmp(&b))
.unwrap()
.clone();
},
move |err| {
println!("Audio error: {:?}", err);
},
)
.unwrap();