feat: use canvas to render png
This commit is contained in:
parent
1a27172cc9
commit
5f51d0b1d6
|
@ -0,0 +1,2 @@
|
|||
node 20
|
||||
yarn latest
|
|
@ -0,0 +1,82 @@
|
|||
# git-cliff ~ default configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
#
|
||||
# Lines starting with "#" are comments.
|
||||
# Configuration options are organized into tables and keys.
|
||||
# See documentation for more information on available options.
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
# postprocessors
|
||||
postprocessors = [
|
||||
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||
]
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"}, # replace issue numbers
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^doc", group = "Documentation" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^style", group = "Styling" },
|
||||
{ message = "^test", group = "Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore|ci", group = "Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "Security" },
|
||||
{ message = "^revert", group = "Revert" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "oldest"
|
||||
# limit the number of commits included in the changelog.
|
||||
# limit_commits = 42
|
File diff suppressed because it is too large
Load Diff
|
@ -12,12 +12,15 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@neodrag/svelte": "^1.2.3",
|
||||
"@tauri-apps/api": "^1.0.2"
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"animejs": "^3.2.1",
|
||||
"image-js": "^0.35.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"@tauri-apps/cli": "^1.1.1",
|
||||
"@tauri-apps/cli": "^1.5.2",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/animejs": "^3.1.8",
|
||||
"@types/node": "^18.7.10",
|
||||
"sass": "^1.54.8",
|
||||
"svelte": "^3.49.0",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "cathode-tube"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
description = "A small PNGTubing app"
|
||||
authors = ["AnActualEmerald"]
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/AnActualEmerald/cathode"
|
||||
|
@ -32,12 +32,12 @@ lto = true
|
|||
codegen-units = 1
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.2.0", features = [] }
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2.0", 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"] }
|
||||
tauri = { version = "1", features = [ "protocol-asset", "window-center", "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.1", features = ["jack"] }
|
||||
ray_format = {path = "../ray_format", version = "~0.1.0"}
|
||||
anyhow = "1.0.66"
|
||||
|
@ -46,9 +46,10 @@ env_logger = "0.9.3"
|
|||
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"
|
||||
figment = { version = "0.10.11", features = ["toml", "env"] }
|
||||
base64 = "0.21.4"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
|
|
@ -12,12 +12,12 @@ use cpal::InputCallbackInfo;
|
|||
use log::debug;
|
||||
use tauri::Window;
|
||||
|
||||
pub async fn monitor(
|
||||
pub fn monitor(
|
||||
window: Window,
|
||||
threshold: Arc<Mutex<f32>>,
|
||||
level: Arc<Mutex<f32>>,
|
||||
sens: Arc<Mutex<f32>>,
|
||||
) {
|
||||
) -> ! {
|
||||
let device = initialize().expect("Unable to init audio");
|
||||
debug!("Using device {}", device.name().unwrap());
|
||||
let config = device.default_input_config().unwrap();
|
||||
|
@ -48,13 +48,14 @@ pub async fn monitor(
|
|||
|
||||
stream.play().expect("Error creating input stream");
|
||||
|
||||
// The stream will end if it goes out of scope, so just dwell here
|
||||
// The stream is closed when dropped, so just dwell here
|
||||
loop {
|
||||
sleep(Duration::from_secs(5000));
|
||||
sleep(Duration::from_millis(10000));
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize() -> Result<Device> {
|
||||
debug!("Hey there");
|
||||
let host = cpal::default_host();
|
||||
host.default_input_device()
|
||||
.ok_or_else(|| anyhow!("No default output device"))
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use figment::{
|
||||
providers::{Env, Format, Toml},
|
||||
Figment,
|
||||
};
|
||||
use log::{debug, error, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::api::path::config_dir;
|
||||
|
@ -55,56 +59,33 @@ impl Display for BGColor {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_config_dir() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("cathode"))
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
config_dir()
|
||||
.expect("Unable to get config directory")
|
||||
.join("cathode")
|
||||
}
|
||||
|
||||
pub fn load_config() -> Config {
|
||||
if let Some(path) = get_config_dir() {
|
||||
use std::fs;
|
||||
if let Ok(true) = path.try_exists() {
|
||||
let raw = fs::read_to_string(path.join("config.toml")).unwrap_or_else(|f| {
|
||||
warn!("Failed to load config file: {}", f);
|
||||
String::new()
|
||||
});
|
||||
trace!("Using config: {}", raw);
|
||||
match toml::from_str::<Config>(&raw) {
|
||||
Ok(c) => {
|
||||
trace!("parsed config: {:#?}", c);
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unable to parse config file: {}", e);
|
||||
Config::default()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = fs::create_dir_all(&path) {
|
||||
debug!("{:?}", e);
|
||||
error!("Failed to create config directory");
|
||||
error!("{}", e);
|
||||
} else {
|
||||
debug!("Created config dir at {}", path.display())
|
||||
}
|
||||
Figment::new()
|
||||
.merge(Toml::file(get_config_dir().join("config.toml")))
|
||||
.merge(Env::prefixed("CATHODE_"))
|
||||
.extract()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Error while loading config: {e}");
|
||||
Config::default()
|
||||
}
|
||||
} else {
|
||||
debug!("Using default config");
|
||||
Config::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_config(config: &Config) -> Result<()> {
|
||||
use std::fs;
|
||||
let raw = toml::to_string_pretty(config)?;
|
||||
if let Some(dir) = get_config_dir() {
|
||||
use std::fs;
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
debug!("Writing config");
|
||||
trace!("Config: {}", raw);
|
||||
let path = dir.join("config.toml");
|
||||
fs::write(&path, raw)?;
|
||||
let dir = get_config_dir();
|
||||
if !dir.exists() {
|
||||
fs::create_dir_all(&dir)?;
|
||||
}
|
||||
debug!("Writing config");
|
||||
trace!("Config: {}", raw);
|
||||
let path = dir.join("config.toml");
|
||||
fs::write(&path, raw)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::Cursor;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use base64_url as base64;
|
||||
use anyhow::Result;
|
||||
use base64::prelude::*;
|
||||
use image::ImageFormat;
|
||||
use log::{debug, trace};
|
||||
use log::{debug, error};
|
||||
use ray_format::Ray;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::api::dialog::blocking::FileDialogBuilder;
|
||||
use tauri::api::path::{cache_dir, home_dir, picture_dir};
|
||||
|
||||
const OBJ_URL: &'static str = "data:image/png;base64,";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub(crate) struct WebRay {
|
||||
frames: [String; 4],
|
||||
|
@ -24,16 +24,25 @@ pub(crate) async fn open_image() -> Option<String> {
|
|||
debug!("Opening iamge dialog...");
|
||||
let path = FileDialogBuilder::new()
|
||||
.add_filter("Images", &["png", "jpg"])
|
||||
.set_directory(picture_dir().unwrap_or_else(|| home_dir().unwrap()))
|
||||
.set_directory(
|
||||
picture_dir().unwrap_or_else(|| home_dir().expect("No home directory for user")),
|
||||
)
|
||||
.set_title("Select an image")
|
||||
.pick_file()?;
|
||||
if let Ok(b) = image::open(path) {
|
||||
let mut buf = Cursor::new(vec![]);
|
||||
b.write_to(&mut buf, ImageFormat::Png).unwrap();
|
||||
let encoded = base64::encode(buf.get_ref());
|
||||
trace!("Encoded: {:?}", encoded);
|
||||
if let Ok(mut b) = image::open(&path) {
|
||||
if b.width() > 600 || b.height() > 400 {
|
||||
b = b.resize(600, 400, image::imageops::FilterType::Lanczos3);
|
||||
}
|
||||
let mut loaded = cache_dir()
|
||||
.expect("Unable to get cache dir")
|
||||
.join("cathode")
|
||||
.join("loaded")
|
||||
.join(path.file_stem().unwrap_or_else(|| OsStr::new("image")));
|
||||
loaded.set_extension("png");
|
||||
let mut file = File::create(&loaded).expect("Unable to create file");
|
||||
b.write_to(&mut file, ImageFormat::Png).unwrap();
|
||||
|
||||
Some(format!("{}{}", OBJ_URL, encoded))
|
||||
loaded.to_str().map(|v| v.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -62,9 +71,9 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
|
|||
debug!("Frame {} was empty", i);
|
||||
continue;
|
||||
}
|
||||
let encoded = base64::encode(&f);
|
||||
let encoded = BASE64_STANDARD.encode(f);
|
||||
|
||||
frames[i as usize] = format!("{}{}", OBJ_URL, encoded);
|
||||
frames[i as usize] = encoded;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,16 +83,10 @@ pub(crate) fn load_ray(path: impl AsRef<Path>) -> Option<WebRay> {
|
|||
}
|
||||
}
|
||||
|
||||
fs::write(
|
||||
cache_dir().unwrap().join("cathode").join("last_selected"),
|
||||
path.as_ref()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
if let Err(e) = cache_loaded_ray(path.as_ref()) {
|
||||
error!("Error caching loaded ray: {e}");
|
||||
}
|
||||
|
||||
Some(WebRay { frames, meta })
|
||||
}
|
||||
|
||||
|
@ -99,18 +102,42 @@ pub(crate) async fn save_ray(ray: WebRay) -> Result<(), String> {
|
|||
let mut res = Ray::default();
|
||||
|
||||
for (i, f) in ray.frames.iter().enumerate() {
|
||||
let stripped = f.strip_prefix(OBJ_URL).unwrap_or(f);
|
||||
let decoded = base64::decode(stripped).unwrap();
|
||||
res.set_frame(i as usize, decoded);
|
||||
if f == "" {
|
||||
continue;
|
||||
}
|
||||
match BASE64_STANDARD.decode(f) {
|
||||
Ok(decoded) => {
|
||||
res.set_frame(i as usize, decoded);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in ray.meta {
|
||||
res.add_meta(k, v);
|
||||
}
|
||||
|
||||
cache_loaded_ray(&path).map_err(|e| e.to_string())?;
|
||||
|
||||
res.save(&path)
|
||||
.map_err(|e| format!("Failed to save ray file: {}", e))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_loaded_ray(path: impl AsRef<Path>) -> Result<()> {
|
||||
fs::write(
|
||||
cache_dir().unwrap().join("cathode").join("last_selected"),
|
||||
path.as_ref()
|
||||
.canonicalize()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -44,20 +44,18 @@ fn main() {
|
|||
|
||||
if let Some(d) = cache_dir() {
|
||||
use std::fs;
|
||||
debug!("Ensuring cache directories exist");
|
||||
|
||||
fs::create_dir_all(d.join("cathode").join("loaded")).expect("Unable to create cache dir");
|
||||
if let Ok(s) = fs::read_to_string(d.join("cathode").join("last_selected")) {
|
||||
debug!("Found selected ray in cache");
|
||||
*ray.lock().unwrap() = Some(Path::new(&s).to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
// Create the config directory if needed
|
||||
debug!("Loading existing config");
|
||||
let config = config::load_config();
|
||||
|
||||
{
|
||||
*blink_interval.lock().unwrap() = config.blink_interval;
|
||||
*sens.lock().unwrap() = config.mic_sens;
|
||||
}
|
||||
|
||||
let current_conf = Arc::new(Mutex::new(config));
|
||||
|
||||
let config = current_conf.clone();
|
||||
|
@ -80,11 +78,17 @@ fn main() {
|
|||
})
|
||||
.setup(move |app| {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
monitor(window, threshold, level, sens).await;
|
||||
monitor(window, threshold, level, sens);
|
||||
});
|
||||
|
||||
let window = app.get_window("main").unwrap();
|
||||
window
|
||||
.emit_all("reload-config", "")
|
||||
.expect("Failed to send window event");
|
||||
#[cfg(debug_assertions)]
|
||||
window.open_devtools();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if rand::random() {
|
||||
|
|
|
@ -28,18 +28,27 @@
|
|||
"fs": {
|
||||
"scope": [
|
||||
"$PUBLIC/*",
|
||||
"$CONFIG/*"
|
||||
"$CONFIG/*",
|
||||
"$CACHE/cathode/*",
|
||||
"$CACHE/cathode/loaded/*"
|
||||
],
|
||||
"readFile": true,
|
||||
"readDir": true,
|
||||
"createDir": true,
|
||||
"writeFile": true
|
||||
},
|
||||
"protocol": {
|
||||
"assetScope": [
|
||||
"$CACHE/cathode/loaded/*"
|
||||
],
|
||||
"asset": true
|
||||
},
|
||||
"window": {
|
||||
"setMinSize": true,
|
||||
"setMaxSize": true,
|
||||
"minimize": true,
|
||||
"unminimize": true
|
||||
"unminimize": true,
|
||||
"center": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
@ -80,7 +89,7 @@
|
|||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": "default-src 'self'; img-src 'self'; asset: https://asset.localhost"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
|
|
|
@ -11,30 +11,32 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { frames } from "../store";
|
||||
import { invoke } from "@tauri-apps/api";
|
||||
import { fs, invoke } from "@tauri-apps/api";
|
||||
import { fade } from "svelte/transition";
|
||||
import Context from "./context.svelte";
|
||||
import LoadingSVG from "/src/loading.svg";
|
||||
import Image from "image-js";
|
||||
export let index: number;
|
||||
|
||||
let menuTimeout: NodeJS.Timeout | null = null;
|
||||
let showMenu = false;
|
||||
let src = "";
|
||||
let src: string | null;
|
||||
|
||||
$: {
|
||||
src = $frames[index];
|
||||
src = $frames[index] ? $frames[index].toDataURL() : null;
|
||||
}
|
||||
|
||||
const openImage = async () => {
|
||||
loading[index] = true;
|
||||
const path = (await invoke("open_image")) as string;
|
||||
if (path) {
|
||||
$frames[index] = path;
|
||||
const data = await fs.readBinaryFile(path);
|
||||
$frames[index] = await Image.load(data);
|
||||
}
|
||||
loading[index] = false;
|
||||
};
|
||||
const clearImage = () => {
|
||||
$frames[index] = "";
|
||||
$frames[index] = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -108,18 +110,20 @@
|
|||
background-color: darken($color: $bg, $amount: 5%);
|
||||
}
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
border: 2px solid black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $bg;
|
||||
width: 15vh;
|
||||
height: 15vh;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 15vh;
|
||||
height: 15vh;
|
||||
user-select: none;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
|
|
|
@ -55,14 +55,19 @@
|
|||
id="sens"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
max="1.0"
|
||||
value={$config.mic_sens}
|
||||
on:change={(e) => {
|
||||
$config.mic_sens = e.currentTarget.valueAsNumber;
|
||||
}}
|
||||
max="5.0"
|
||||
bind:value={$config.mic_sens}
|
||||
/>
|
||||
<span>{Math.trunc($config.mic_sens * 100)}%</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="save"
|
||||
on:click={async () => {
|
||||
await invoke("save_current_config");
|
||||
onClose();
|
||||
}}>Save</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -110,4 +115,12 @@
|
|||
justify-content: space-between;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#save {
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
bottom: 0.25rem;
|
||||
width: 25%;
|
||||
height: 5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,15 +2,20 @@
|
|||
import { frames } from "../store";
|
||||
import { onMount } from "svelte";
|
||||
import { appWindow } from "@tauri-apps/api/window";
|
||||
import type { Image } from "image-js";
|
||||
import anime from "animejs";
|
||||
|
||||
let src = "";
|
||||
export let buf = 0;
|
||||
export let open = false;
|
||||
export let threshold = 50;
|
||||
let closed = false;
|
||||
let blink = false;
|
||||
let inAnim = "jump-in";
|
||||
let outAnim = "none";
|
||||
$: bitMaps = [null, null, null, null];
|
||||
$: currFrame = 0;
|
||||
|
||||
$: src = bitMaps[currFrame];
|
||||
|
||||
let pos = { x: 0, y: 0 };
|
||||
|
||||
$: {
|
||||
if (buf < threshold) {
|
||||
|
@ -21,19 +26,58 @@
|
|||
|
||||
$: {
|
||||
if (closed) {
|
||||
src = $frames[0];
|
||||
currFrame = 0;
|
||||
} else if (open) {
|
||||
src = $frames[1];
|
||||
currFrame = 1;
|
||||
}
|
||||
|
||||
if (blink && closed) {
|
||||
src = $frames[2] ? $frames[2] : $frames[0];
|
||||
currFrame = $frames[2] ? 2 : 0;
|
||||
} else if (blink && open) {
|
||||
src = $frames[3] ? $frames[3] : $frames[1];
|
||||
currFrame = $frames[3] ? 3 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (open) {
|
||||
anime({
|
||||
targets: pos,
|
||||
y: [
|
||||
{ value: 50, duration: 200 },
|
||||
{ value: 0, duration: 200 },
|
||||
],
|
||||
autoplay: true,
|
||||
easing: "easeOutCubic",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// $: {
|
||||
// if (closed)
|
||||
// anime({
|
||||
// targets: pos,
|
||||
// y: [
|
||||
// { value: 100, duration: 100 },
|
||||
// { value: 0, duration: 100 },
|
||||
// ],
|
||||
// autoplay: true,
|
||||
// easing: "easeInOutCubic",
|
||||
// });
|
||||
// }
|
||||
|
||||
const createBitmaps = async (f: Array<Image | null>) => {
|
||||
return await Promise.all(
|
||||
f.map(async (v) => (v ? await createImageBitmap(await v.toBlob()) : null))
|
||||
);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
bitMaps = await createBitmaps($frames);
|
||||
console.log("bitMaps: ", bitMaps);
|
||||
frames.subscribe(async (f) => {
|
||||
bitMaps = await createBitmaps(f);
|
||||
});
|
||||
|
||||
await appWindow.listen("mouth-open", () => {
|
||||
buf = 100;
|
||||
open = true;
|
||||
|
@ -49,57 +93,43 @@
|
|||
blink = true;
|
||||
setTimeout(() => (blink = false), 100);
|
||||
});
|
||||
|
||||
await appWindow.onResized(updateCanvasSize);
|
||||
|
||||
updateCanvasSize();
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const update = async () => {
|
||||
try {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(
|
||||
src,
|
||||
canvas.width / 2 - pos.x - src.width / 2,
|
||||
canvas.height / 2 - pos.y - src.height / 2
|
||||
);
|
||||
} catch {}
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
</script>
|
||||
|
||||
{#if src}
|
||||
<img {src} alt="tuber" class:open class:closed class="{inAnim} {outAnim}" />
|
||||
{/if}
|
||||
<canvas bind:this={canvas} />
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes jump-out {
|
||||
0% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-52%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes jump-in {
|
||||
0% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-52%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
canvas {
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
top: 50vh;
|
||||
left: calc(50vw - 200px);
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.closed.jump-out {
|
||||
animation: jump-out;
|
||||
animation-duration: 0.2s;
|
||||
}
|
||||
|
||||
.open.jump-in {
|
||||
animation: jump-in;
|
||||
animation-duration: 0.2s;
|
||||
translate: -50% -50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
10
src/store.ts
10
src/store.ts
|
@ -1,16 +1,18 @@
|
|||
import {writable} from "svelte/store";
|
||||
import type { Image } from "image-js";
|
||||
export type BGColor ="transparent" | "blue" | "green" | "pink" | {custom: string} ;
|
||||
|
||||
export type Meta = {
|
||||
|
||||
export type Meta = {
|
||||
threshold: string | null;
|
||||
closeThreshold: string | null;
|
||||
};
|
||||
|
||||
export class WebRay {
|
||||
meta: Meta;
|
||||
frames: string[];
|
||||
frames: Array<string | null>;
|
||||
public constructor(
|
||||
frames: string[] = [],
|
||||
frames: Array<string | null> = [null, null, null, null],
|
||||
meta: Meta = { threshold: null, closeThreshold: null }
|
||||
) {
|
||||
this.frames = frames;
|
||||
|
@ -33,5 +35,5 @@ export class Config {
|
|||
|
||||
|
||||
|
||||
export let frames = writable(new Array<string>(4));
|
||||
export let frames = writable<Array<Image | null>>([null, null, null, null]);
|
||||
export let config = writable(new Config());
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { appWindow, PhysicalSize } from "@tauri-apps/api/window";
|
||||
// import { listen } from "@tauri-apps/api/event";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { tick } from "svelte";
|
||||
import { frames, WebRay } from "../store";
|
||||
import { quintInOut } from "svelte/easing";
|
||||
import { Image } from "image-js";
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
|
||||
//components
|
||||
|
@ -24,7 +24,7 @@
|
|||
import Settings from "../components/settings.svelte";
|
||||
// import Devices from "../components/devices.svelte";
|
||||
|
||||
let monitorTimer: NodeJS.Timer;
|
||||
let monitorTimer: string | number | NodeJS.Timeout;
|
||||
|
||||
let active = false;
|
||||
let activation = 0;
|
||||
|
@ -52,9 +52,20 @@
|
|||
});
|
||||
}
|
||||
|
||||
const openRay = (ray: WebRay) => {
|
||||
const openRay = async (ray: WebRay) => {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
$frames[i] = ray.frames[i];
|
||||
if (!ray.frames[i]) {
|
||||
$frames[i] = null;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const image = await Image.load(
|
||||
`data:image/png;base64,${ray.frames[i]}`
|
||||
);
|
||||
$frames[i] = image;
|
||||
} catch (e) {
|
||||
await invoke("log", { msg: "Error loading blob: " + e.toString() });
|
||||
}
|
||||
}
|
||||
if (ray.meta.threshold) {
|
||||
$threshold = parseFloat(ray.meta.threshold);
|
||||
|
@ -65,8 +76,8 @@
|
|||
};
|
||||
|
||||
onMount(async () => {
|
||||
await appWindow.center();
|
||||
await appWindow.setMinSize(new PhysicalSize(720, 600));
|
||||
|
||||
focusUnlisten = await appWindow.onFocusChanged(({ payload: focused }) => {
|
||||
$transparent = !focused;
|
||||
});
|
||||
|
@ -100,26 +111,28 @@
|
|||
const saveRay = async () => {
|
||||
let fr = new Array<string>(4);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
$frames[i] ? (fr[i] = $frames[i]) : (fr[i] = "");
|
||||
$frames[i] ? (fr[i] = $frames[i].toBase64() as string) : (fr[i] = "");
|
||||
}
|
||||
fr.map((e) => (e ? e : ""));
|
||||
const ray = {
|
||||
frames: [fr[0], fr[1], fr[2], fr[3]],
|
||||
frames: fr,
|
||||
meta: {
|
||||
threshold: $threshold.toString(),
|
||||
closeThreshold: closeThreshold.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
// await invoke("log", { msg: `Saving ray: ${JSON.stringify(ray)}` });
|
||||
invoke("save_ray", { ray })
|
||||
.then()
|
||||
.catch((e) => invoke("log", { msg: e }).then());
|
||||
await invoke("log", { msg: `Saving ray: ${JSON.stringify(ray)}` });
|
||||
try {
|
||||
await invoke("save_ray", { ray });
|
||||
} catch (e) {
|
||||
await invoke("log", { msg: e });
|
||||
}
|
||||
};
|
||||
|
||||
const loadRay = async () => {
|
||||
const ray: WebRay = await invoke("open_ray");
|
||||
openRay(ray);
|
||||
await openRay(ray);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ export default defineConfig({
|
|||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
cors: true
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
|
|
Loading…
Reference in New Issue