load, save, and new buttons
|
@ -8,6 +8,7 @@ node_modules
|
|||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
target
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
|
|
@ -26,10 +26,12 @@
|
|||
"@types/omggif": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vite-pwa/sveltekit": "^0.2.7",
|
||||
"autoprefixer": "10.4.16",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"kolorist": "^1.8.0",
|
||||
"postcss": "8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
|
@ -40,9 +42,12 @@
|
|||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vite-plugin-tailwind-purgecss": "^0.1.3",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vite-plugin-wasm": "^3.2.2"
|
||||
"vite-plugin-wasm": "^3.2.2",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
@ -25,8 +25,6 @@ gtk3 = ">= 3"
|
|||
webkit2gtk3 = ">= 2"
|
||||
|
||||
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "s"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { Gif } from '$lib/gifler';
|
||||
import { loadFrame, loadImage, loadRay } from '$lib/io';
|
||||
import { debug } from '$lib/logging';
|
||||
import { keepFocused } from '$lib/state';
|
||||
import { frames, mode } from '$lib/store';
|
||||
import { base64ToArrayBuffer } from '$lib/utils';
|
||||
import Image from 'image-js';
|
||||
|
||||
let fileLoader: HTMLInputElement;
|
||||
let files: FileList;
|
||||
|
||||
$: loading = false;
|
||||
$: $keepFocused = loading;
|
||||
|
||||
const handleLoad = () => {
|
||||
if (mode === 'web') {
|
||||
fileLoader.click();
|
||||
loading = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebLoad = () => {
|
||||
const file = files.item(0);
|
||||
if (file) finishLoad(file);
|
||||
|
||||
fileLoader.value = '';
|
||||
};
|
||||
|
||||
const finishLoad = async (data: File | Blob) => {
|
||||
const ray = await loadRay(data);
|
||||
|
||||
const expr = ray?.get_expr(0);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const raw = expr?.get_frame(i);
|
||||
if (!raw) {
|
||||
$frames[i] = null;
|
||||
continue;
|
||||
}
|
||||
const image = await loadFrame(raw);
|
||||
|
||||
if (image instanceof Image) {
|
||||
$frames[i] = {
|
||||
kind: 'still',
|
||||
value: image
|
||||
};
|
||||
} else if (image instanceof Gif) {
|
||||
$frames[i] = {
|
||||
kind: 'GIF',
|
||||
value: image,
|
||||
data: new Uint8Array(base64ToArrayBuffer(raw))
|
||||
};
|
||||
} else {
|
||||
$frames[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
ray?.free();
|
||||
loading = false
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if mode === 'web'}
|
||||
<input type="file" class="hidden" bind:this={fileLoader} bind:files on:change={handleWebLoad} />
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-md variant-ringed-surface" on:click={handleLoad}>Load</button>
|
|
@ -12,6 +12,8 @@
|
|||
});
|
||||
|
||||
const saveCurrent = async () => {
|
||||
// this feels inefficient but we're basically just mapping the current expression to
|
||||
// wasm memory
|
||||
const ray = new UnpackedRay();
|
||||
const expr = new Expression();
|
||||
for (let i = 0; i < $frames.length; i++) {
|
||||
|
@ -36,6 +38,9 @@
|
|||
}
|
||||
|
||||
|
||||
// don't forget to free
|
||||
ray.free();
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
import { Image } from 'image-js';
|
||||
import { mode } from '$lib/store';
|
||||
import gifler, { Gif } from './gifler';
|
||||
import { load_ray, type UnpackedRay } from '@cathode/cathode-ray';
|
||||
import { base64ToArrayBuffer } from '$lib/utils';
|
||||
|
||||
export const loadFrame = async (raw: string): Promise<Gif | Image | null> => {
|
||||
const blob = new Blob([base64ToArrayBuffer(raw)]);
|
||||
try {
|
||||
return await loadImage(blob);
|
||||
} catch {
|
||||
try {
|
||||
return await loadGif(blob);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loadImage = async (data: File | Blob): Promise<Image | null> => {
|
||||
if (mode === 'web') {
|
||||
|
@ -10,21 +25,17 @@ export const loadImage = async (data: File | Blob): Promise<Image | null> => {
|
|||
const raw = reader.result as ArrayBuffer | null;
|
||||
if (!raw) return null;
|
||||
let img = await Image.load(raw);
|
||||
if (img.width > 600 || img.height > 400){
|
||||
img = img.resize({
|
||||
if (img.width > 600 || img.height > 400) {
|
||||
img = img.resize({
|
||||
width: 600
|
||||
})
|
||||
}
|
||||
resolve(
|
||||
img
|
||||
);
|
||||
});
|
||||
}
|
||||
resolve(img);
|
||||
};
|
||||
reader.readAsArrayBuffer(data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
@ -36,14 +47,25 @@ export const loadGif = async (data: File | Blob): Promise<Gif | null> => {
|
|||
const raw = reader.result as string | null;
|
||||
if (!raw) return resolve(null);
|
||||
let img = gifler(raw);
|
||||
resolve(
|
||||
img
|
||||
);
|
||||
resolve(img);
|
||||
};
|
||||
reader.readAsDataURL(data);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadRay = async (data: File | Blob): Promise<UnpackedRay | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const raw = reader.result as ArrayBuffer | null;
|
||||
if (!raw) return resolve(null);
|
||||
const ray = load_ray(new Uint8Array(raw));
|
||||
resolve(ray ?? null);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(data);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import {writable} from "svelte/store";
|
|||
import type { Image } from "image-js";
|
||||
import type { Gif } from "$lib/gifler";
|
||||
import { info } from "$lib/logging";
|
||||
import { TAURI_MODE } from "$env/static/public";
|
||||
export type BGColor ="transparent" | "blue" | "green" | "pink" | {custom: string} ;
|
||||
|
||||
export type Meta = {
|
||||
|
@ -50,6 +51,6 @@ export class Config {
|
|||
|
||||
export let frames = writable<Array<FrameData | null>>([null, null, null, null]);
|
||||
export let config = writable(new Config());
|
||||
export const mode = window?.__TAURI__ ? "tauri" : "web";
|
||||
export const mode = TAURI_MODE === "tauri" ? "tauri" : "web";
|
||||
|
||||
info(`Running app in ${mode} mode`);
|
||||
|
|
|
@ -5,6 +5,15 @@ export function bytesToBase64(bytes: Uint8Array): string {
|
|||
return btoa(binString);
|
||||
}
|
||||
|
||||
export function base64ToArrayBuffer(base64: string) {
|
||||
var binaryString = atob(base64);
|
||||
var bytes = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export const getBase64 = async (obj: FrameData) => {
|
||||
if (obj.kind === "still") {
|
||||
return await obj.value.toBase64();
|
||||
|
|
|
@ -12,8 +12,14 @@
|
|||
import { onDestroy, onMount } from 'svelte';
|
||||
import { active, initState, closeThreshold, activationLevel, transparent } from '$lib/state';
|
||||
import SaveButton from '$lib/components/SaveButton.svelte';
|
||||
import LoadButton from '$lib/components/LoadButton.svelte';
|
||||
import { frames } from "$lib/store"
|
||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||
|
||||
// import { pwaInfo } from 'virtual:pwa-info';
|
||||
|
||||
// $: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : '' ;
|
||||
|
||||
let deinitAudio: NodeJS.Timeout | undefined = undefined;
|
||||
onMount(async () => {
|
||||
deinitAudio = await initAudio();
|
||||
|
@ -24,9 +30,17 @@
|
|||
if (deinitAudio) clearInterval(deinitAudio);
|
||||
});
|
||||
|
||||
const clearFrames = () => {
|
||||
frames.update(() => [null, null, null, null]);
|
||||
}
|
||||
|
||||
$: bgColor = $transparent ? 'bg-green-800' : 'bg-surface-900';
|
||||
</script>
|
||||
|
||||
<!-- <svelte:head>
|
||||
{@html webManifestLink}
|
||||
</svelte:head> -->
|
||||
|
||||
<div class="{bgColor} w-screen h-screen" id="container">
|
||||
<slot />
|
||||
{#if !$transparent}
|
||||
|
@ -39,9 +53,9 @@
|
|||
}}
|
||||
class="flex w-full justify-center absolute top-4 gap-4"
|
||||
>
|
||||
<button class="btn btn-md variant-ringed-surface">New</button>
|
||||
<button class="btn btn-md variant-ringed-surface" on:click={clearFrames}>New</button>
|
||||
<SaveButton />
|
||||
<button class="btn btn-md variant-ringed-surface">Load</button>
|
||||
<LoadButton />
|
||||
</div>
|
||||
<div
|
||||
transition:fly={{
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference no-default-lib="true"/>
|
||||
/// <reference lib="esnext" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import {build, files, version} from "$service-worker";
|
||||
|
||||
const CACHE = `cache-${version}`;
|
||||
|
||||
const ASSETS = [
|
||||
...build,
|
||||
...files,
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
async function cacheFiles() {
|
||||
const c = await caches.open(CACHE);
|
||||
await c.addAll(ASSETS);
|
||||
}
|
||||
|
||||
event.waitUntil(cacheFiles());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// Remove previous cached data from disk
|
||||
async function deleteOldCaches() {
|
||||
for (const key of await caches.keys()) {
|
||||
if (key !== CACHE) await caches.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
event.waitUntil(deleteOldCaches());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// ignore POST requests etc
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
async function respond() {
|
||||
const url = new URL(event.request.url);
|
||||
const cache = await caches.open(CACHE);
|
||||
|
||||
// `build`/`files` can always be served from the cache
|
||||
if (ASSETS.includes(url.pathname)) {
|
||||
return cache.match(url.pathname);
|
||||
}
|
||||
|
||||
// for everything else, try the network first, but
|
||||
// fall back to the cache if we're offline
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (response.status === 200) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
return cache.match(event.request);
|
||||
}
|
||||
}
|
||||
|
||||
event.respondWith(respond());
|
||||
});
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 562 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -1,56 +0,0 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 204.481 204.481" style="enable-background:new 0 0 204.481 204.481;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M162.116,38.31c0.163-0.215,0.315-0.438,0.454-0.67c0.033-0.055,0.068-0.109,0.1-0.164
|
||||
c0.156-0.276,0.297-0.561,0.419-0.857c0.014-0.034,0.024-0.069,0.038-0.104c0.102-0.26,0.188-0.528,0.261-0.801
|
||||
c0.019-0.069,0.037-0.137,0.053-0.207c0.068-0.288,0.124-0.581,0.157-0.881c0.002-0.017,0.006-0.034,0.008-0.052
|
||||
c0.028-0.262,0.043-0.527,0.043-0.796V7.5c0-4.142-3.358-7.5-7.5-7.5H48.332c-4.142,0-7.5,3.358-7.5,7.5v26.279
|
||||
c0,0.269,0.016,0.534,0.043,0.796c0.002,0.017,0.006,0.034,0.008,0.052c0.034,0.3,0.089,0.593,0.157,0.881
|
||||
c0.016,0.069,0.035,0.138,0.053,0.207c0.073,0.273,0.159,0.541,0.261,0.801c0.013,0.034,0.024,0.069,0.038,0.104
|
||||
c0.121,0.296,0.262,0.581,0.419,0.857c0.032,0.056,0.067,0.109,0.1,0.164c0.14,0.232,0.291,0.455,0.454,0.67
|
||||
c0.027,0.035,0.047,0.074,0.074,0.109l50.255,63.821l-50.255,63.821c-0.028,0.035-0.047,0.074-0.074,0.109
|
||||
c-0.163,0.215-0.315,0.438-0.454,0.67c-0.033,0.055-0.068,0.109-0.1,0.164c-0.156,0.276-0.297,0.561-0.419,0.857
|
||||
c-0.014,0.034-0.024,0.069-0.038,0.104c-0.102,0.26-0.188,0.528-0.261,0.801c-0.019,0.069-0.037,0.137-0.053,0.207
|
||||
c-0.068,0.288-0.124,0.581-0.157,0.881c-0.002,0.017-0.006,0.034-0.008,0.052c-0.028,0.262-0.043,0.527-0.043,0.796v26.279
|
||||
c0,4.142,3.358,7.5,7.5,7.5h107.817c4.142,0,7.5-3.358,7.5-7.5v-26.279c0-0.269-0.016-0.534-0.043-0.796
|
||||
c-0.002-0.017-0.006-0.034-0.008-0.052c-0.034-0.3-0.089-0.593-0.157-0.881c-0.016-0.069-0.035-0.138-0.053-0.207
|
||||
c-0.073-0.273-0.159-0.541-0.261-0.801c-0.013-0.034-0.024-0.069-0.038-0.104c-0.121-0.296-0.262-0.581-0.419-0.857
|
||||
c-0.032-0.056-0.067-0.109-0.1-0.164c-0.14-0.232-0.291-0.455-0.454-0.67c-0.027-0.035-0.047-0.074-0.074-0.109l-50.255-63.821
|
||||
l50.255-63.821C162.07,38.385,162.089,38.346,162.116,38.31z M148.649,15v11.279H55.832V15H148.649z M55.832,189.481v-11.279
|
||||
h92.817v11.279H55.832z M140.698,163.202H63.784l38.457-48.838L140.698,163.202z M102.241,90.118L63.784,41.279h76.914
|
||||
L102.241,90.118z"/>
|
||||
</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: 2.5 KiB |
|
@ -19,7 +19,7 @@ const config = {
|
|||
env: {
|
||||
publicPrefix: 'TAURI_'
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
export default config;
|
|
@ -8,7 +8,11 @@
|
|||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"types": [
|
||||
"vite-plugin-pwa/client",
|
||||
"vite-plugin-pwa/info"
|
||||
]
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
|
|
|
@ -2,6 +2,7 @@ import { purgeCss } from 'vite-plugin-tailwind-purgecss';
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import { SvelteKitPWA } from "@vite-pwa/sveltekit";
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
|
|