diff options
author | 2025-08-18 18:30:04 +0200 | |
---|---|---|
committer | 2025-08-18 18:30:04 +0200 | |
commit | c2ed0f84cef644ba28096f01ec14764114b68c0f (patch) | |
tree | fbdc236ba8ba646cf27fc51d250ea018a2fa7f25 /src | |
download | pixelsvg-c2ed0f84cef644ba28096f01ec14764114b68c0f.tar.gz pixelsvg-c2ed0f84cef644ba28096f01ec14764114b68c0f.tar.bz2 pixelsvg-c2ed0f84cef644ba28096f01ec14764114b68c0f.tar.lz pixelsvg-c2ed0f84cef644ba28096f01ec14764114b68c0f.zip |
Diffstat (limited to 'src')
-rw-r--r-- | src/app.d.ts | 13 | ||||
-rw-r--r-- | src/app.html | 11 | ||||
-rw-r--r-- | src/lib/Alerts.svelte | 184 | ||||
-rw-r--r-- | src/lib/assets/favicon.old.svg | 9 | ||||
-rw-r--r-- | src/lib/assets/favicon.png | bin | 0 -> 1149 bytes | |||
-rw-r--r-- | src/lib/assets/favicon.svg | 1 | ||||
-rw-r--r-- | src/lib/index.ts | 1 | ||||
-rw-r--r-- | src/routes/+layout.svelte | 11 | ||||
-rw-r--r-- | src/routes/+page.svelte | 202 |
9 files changed, 432 insertions, 0 deletions
diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..b0b3788 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + %sveltekit.head% + </head> + <body data-sveltekit-preload-data="hover" class="cs"> + <div style="display: contents">%sveltekit.body%</div> + </body> +</html> diff --git a/src/lib/Alerts.svelte b/src/lib/Alerts.svelte new file mode 100644 index 0000000..1e4641b --- /dev/null +++ b/src/lib/Alerts.svelte @@ -0,0 +1,184 @@ +<!-- +MIT License + +Copyright (c) 2025 memdmp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + --> + +<script lang="ts" module> + let ids = $state([] as number[]); + let idCtr = $state(0); + let dialogObject = $state({ + open: false, + title: '', + content: '', + closeBtn: false, + buttons: [ + { + text: 'Close', + }, + ] as { + text: string; + callback?: () => + | { + success: true; + content: string; + } + | { + success: false; + content: any; + }; + }[], + }); + let closeAlert = (m: string) => void 0; + let cancelAlert = (err?: any) => void 0; + let dialogPromises = [] as Promise<string>[]; + let dialogPromise = Promise.resolve(''); + const createDialogPromise = (setAlertTo?: typeof dialogObject) => ( + dialogPromises.push( + (dialogPromise = new Promise<string>((rs, rj) => { + let oldCloseAlert = closeAlert; + let oldCancelAlert = cancelAlert; + closeAlert = (m) => { + closeAlert = oldCloseAlert; + dialogObject = setAlertTo ?? dialogObject; + rs(m); + }; + cancelAlert = (m) => { + cancelAlert = oldCancelAlert; + dialogObject = setAlertTo ?? dialogObject; + rj(m); + }; + })) + ), + dialogPromise + ); + export const alert = async ( + message: string, + options?: { + title?: string; + /** prioritise over existing alerts */ + priority?: boolean; + /** if we can close the dialog with the x at the top */ + canClose?: boolean; + /** button list */ + buttons?: typeof dialogObject.buttons; + } + ) => { + if (!options?.priority) await Promise.allSettled(dialogPromises); + const oldAlert = dialogObject; + dialogObject = { + open: true, + title: options?.title ?? 'Alert', + closeBtn: options?.canClose ?? true, + buttons: options?.buttons ?? [ + { + text: 'Close', + }, + ], + content: message, + }; + return createDialogPromise(oldAlert); + }; + export const confirm = async ( + message: string, + options?: Omit<Parameters<typeof alert>[1], 'buttons'> + ) => { + const rs = await alert(message, { + title: 'Confirmation', + ...(options ?? {}), + buttons: [ + { + text: 'OK', + callback: () => ({ + success: true, + content: 'OK', + }), + }, + { + text: 'Cancel', + callback: () => ({ + success: true, + content: 'Cancel', + }), + }, + ], + }); + return rs === 'OK'; + }; +</script> + +<script lang="ts"> + import { onDestroy, onMount } from 'svelte'; + + let dialog = $state(null as HTMLDialogElement | null); + let id: number = $state(-1); + onMount(() => { + id = idCtr++; + ids.push(id); + }); + onDestroy(() => (ids = ids.filter((v) => v !== id))); + $effect(() => { + dialog?.showModal(); + }); +</script> + +{#if ids[0] === id && dialogObject.open} + <section> + <dialog bind:this={dialog}> + <form method="dialog"> + <div class="heading"> + <div class="wrapper"> + <div class="icon"></div> + <p class="text">{dialogObject.title}</p> + </div> + <button + class="close" + aria-label="Close" + disabled={!dialogObject.closeBtn} + onclick={() => { + cancelAlert(new Error('User Cancelled Alert')); + }} + ></button> + </div> + <div class="content"> + {dialogObject.content} + </div> + <menu class="footer-btns" style="gap: 4px; display: flex;"> + {#each dialogObject.buttons as button} + <button + onclick={(e) => { + const callback = + button.callback ?? + (() => ({ + success: true, + content: button.text, + })); + const result = callback(); + if (result.success) closeAlert(result.content); + else cancelAlert(result.content); + }}>{button.text}</button + > + {/each} + </menu> + </form> + </dialog> + </section> +{/if} diff --git a/src/lib/assets/favicon.old.svg b/src/lib/assets/favicon.old.svg new file mode 100644 index 0000000..d69dee8 --- /dev/null +++ b/src/lib/assets/favicon.old.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"> + <title>svelte-logo</title> + <path + d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" + style="fill:#BF64CC" /> + <path + d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" + style="fill:#fff" /> +</svg>
\ No newline at end of file diff --git a/src/lib/assets/favicon.png b/src/lib/assets/favicon.png Binary files differnew file mode 100644 index 0000000..d7ac7dd --- /dev/null +++ b/src/lib/assets/favicon.png diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..45f50a4 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox="0 0 24 24" width="256" height="256"><rect fill="#bd61ce2f" x="13" y="1" width="1" height="1"/><rect fill="#bf64cc65" x="14" y="1" width="1" height="1"/><rect fill="#be63cc73" x="15" y="1" width="1" height="1"/><rect fill="#bf64cb54" x="16" y="1" width="1" height="1"/><rect fill="#bb5dc913" x="17" y="1" width="1" height="1"/><rect fill="#bf62cd34" x="11" y="2" width="1" height="1"/><rect fill="#bf64ccc4" x="12" y="2" width="1" height="1"/><rect fill="#bf64cc" x="13" y="2" width="1" height="1"/><rect fill="#bf64cc" x="14" y="2" width="1" height="1"/><rect fill="#bf64cc" x="15" y="2" width="1" height="1"/><rect fill="#bf64cc" x="16" y="2" width="1" height="1"/><rect fill="#be63ccfa" x="17" y="2" width="1" height="1"/><rect fill="#bf63cb94" x="18" y="2" width="1" height="1"/><rect fill="#bf6ad40c" x="19" y="2" width="1" height="1"/><rect fill="#b95cd00b" x="9" y="3" width="1" height="1"/><rect fill="#be64cc8e" x="10" y="3" width="1" height="1"/><rect fill="#bf64ccfc" x="11" y="3" width="1" height="1"/><rect fill="#bf64cc" x="12" y="3" width="1" height="1"/><rect fill="#bf64cc" x="13" y="3" width="1" height="1"/><rect fill="#bf64cc" x="14" y="3" width="1" height="1"/><rect fill="#c26bce" x="15" y="3" width="1" height="1"/><rect fill="#bf64cc" x="16" y="3" width="1" height="1"/><rect fill="#bf64cc" x="17" y="3" width="1" height="1"/><rect fill="#bf64cc" x="18" y="3" width="1" height="1"/><rect fill="#bf64cbd1" x="19" y="3" width="1" height="1"/><rect fill="#c369d211" x="20" y="3" width="1" height="1"/><rect fill="#bf63cd48" x="8" y="4" width="1" height="1"/><rect fill="#be63cbdf" x="9" y="4" width="1" height="1"/><rect fill="#bf64cc" x="10" y="4" width="1" height="1"/><rect fill="#bf64cc" x="11" y="4" width="1" height="1"/><rect fill="#c26bce" x="12" y="4" width="1" height="1"/><rect fill="#e1b5e7" x="13" y="4" width="1" height="1"/><rect fill="#fbf5fc" x="14" y="4" width="1" height="1"/><rect fill="#ffffff" x="15" y="4" width="1" height="1"/><rect fill="#f8edf9" x="16" y="4" width="1" height="1"/><rect fill="#daa5e1" x="17" y="4" width="1" height="1"/><rect fill="#bf64cc" x="18" y="4" width="1" height="1"/><rect fill="#bf64cc" x="19" y="4" width="1" height="1"/><rect fill="#be64cbb2" x="20" y="4" width="1" height="1"/><rect fill="#bb66cc0f" x="6" y="5" width="1" height="1"/><rect fill="#bf63cba4" x="7" y="5" width="1" height="1"/><rect fill="#bf64cc" x="8" y="5" width="1" height="1"/><rect fill="#bf64cc" x="9" y="5" width="1" height="1"/><rect fill="#bf64cc" x="10" y="5" width="1" height="1"/><rect fill="#d190db" x="11" y="5" width="1" height="1"/><rect fill="#f7ecf9" x="12" y="5" width="1" height="1"/><rect fill="#ffffff" x="13" y="5" width="1" height="1"/><rect fill="#ffffff" x="14" y="5" width="1" height="1"/><rect fill="#ffffff" x="15" y="5" width="1" height="1"/><rect fill="#ffffff" x="16" y="5" width="1" height="1"/><rect fill="#ffffff" x="17" y="5" width="1" height="1"/><rect fill="#ddade4" x="18" y="5" width="1" height="1"/><rect fill="#bf64cc" x="19" y="5" width="1" height="1"/><rect fill="#bf64cc" x="20" y="5" width="1" height="1"/><rect fill="#bd65ca3a" x="21" y="5" width="1" height="1"/><rect fill="#bb5dc913" x="5" y="6" width="1" height="1"/><rect fill="#be63cbd7" x="6" y="6" width="1" height="1"/><rect fill="#bf64cc" x="7" y="6" width="1" height="1"/><rect fill="#bf64cc" x="8" y="6" width="1" height="1"/><rect fill="#c571d0" x="9" y="6" width="1" height="1"/><rect fill="#e8c8ed" x="10" y="6" width="1" height="1"/><rect fill="#ffffff" x="11" y="6" width="1" height="1"/><rect fill="#ffffff" x="12" y="6" width="1" height="1"/><rect fill="#ffffff" x="13" y="6" width="1" height="1"/><rect fill="#f5e7f7" x="14" y="6" width="1" height="1"/><rect fill="#deafe5" x="15" y="6" width="1" height="1"/><rect fill="#f7ecf9" x="16" y="6" width="1" height="1"/><rect fill="#ffffff" x="17" y="6" width="1" height="1"/><rect fill="#fbf6fc" x="18" y="6" width="1" height="1"/><rect fill="#c26ccf" x="19" y="6" width="1" height="1"/><rect fill="#bf64cc" x="20" y="6" width="1" height="1"/><rect fill="#be64cb8b" x="21" y="6" width="1" height="1"/><rect fill="#be64ccaa" x="5" y="7" width="1" height="1"/><rect fill="#bf64cc" x="6" y="7" width="1" height="1"/><rect fill="#bf64cc" x="7" y="7" width="1" height="1"/><rect fill="#d497dd" x="8" y="7" width="1" height="1"/><rect fill="#faf3fb" x="9" y="7" width="1" height="1"/><rect fill="#ffffff" x="10" y="7" width="1" height="1"/><rect fill="#ffffff" x="11" y="7" width="1" height="1"/><rect fill="#fdfbfe" x="12" y="7" width="1" height="1"/><rect fill="#dfb2e6" x="13" y="7" width="1" height="1"/><rect fill="#c168cd" x="14" y="7" width="1" height="1"/><rect fill="#bf64cc" x="15" y="7" width="1" height="1"/><rect fill="#d59ade" x="16" y="7" width="1" height="1"/><rect fill="#fffeff" x="17" y="7" width="1" height="1"/><rect fill="#ffffff" x="18" y="7" width="1" height="1"/><rect fill="#cb81d5" x="19" y="7" width="1" height="1"/><rect fill="#bf64cc" x="20" y="7" width="1" height="1"/><rect fill="#bf63ccac" x="21" y="7" width="1" height="1"/><rect fill="#c164c821" x="4" y="8" width="1" height="1"/><rect fill="#be64cbfe" x="5" y="8" width="1" height="1"/><rect fill="#bf64cc" x="6" y="8" width="1" height="1"/><rect fill="#d496dd" x="7" y="8" width="1" height="1"/><rect fill="#fefeff" x="8" y="8" width="1" height="1"/><rect fill="#ffffff" x="9" y="8" width="1" height="1"/><rect fill="#ffffff" x="10" y="8" width="1" height="1"/><rect fill="#f0dcf3" x="11" y="8" width="1" height="1"/><rect fill="#ca7ed5" x="12" y="8" width="1" height="1"/><rect fill="#bf64cc" x="13" y="8" width="1" height="1"/><rect fill="#bf64cc" x="14" y="8" width="1" height="1"/><rect fill="#bf64cc" x="15" y="8" width="1" height="1"/><rect fill="#bf64cc" x="16" y="8" width="1" height="1"/><rect fill="#c97cd4" x="17" y="8" width="1" height="1"/><rect fill="#ecd0f0" x="18" y="8" width="1" height="1"/><rect fill="#c879d3" x="19" y="8" width="1" height="1"/><rect fill="#bf64cc" x="20" y="8" width="1" height="1"/><rect fill="#be64cca2" x="21" y="8" width="1" height="1"/><rect fill="#be63cb5f" x="4" y="9" width="1" height="1"/><rect fill="#bf64cc" x="5" y="9" width="1" height="1"/><rect fill="#bf64cc" x="6" y="9" width="1" height="1"/><rect fill="#f3e1f5" x="7" y="9" width="1" height="1"/><rect fill="#ffffff" x="8" y="9" width="1" height="1"/><rect fill="#fdfafd" x="9" y="9" width="1" height="1"/><rect fill="#daa5e1" x="10" y="9" width="1" height="1"/><rect fill="#bf65cc" x="11" y="9" width="1" height="1"/><rect fill="#bf64cc" x="12" y="9" width="1" height="1"/><rect fill="#bf64cc" x="13" y="9" width="1" height="1"/><rect fill="#c676d2" x="14" y="9" width="1" height="1"/><rect fill="#ca7ed4" x="15" y="9" width="1" height="1"/><rect fill="#c066cd" x="16" y="9" width="1" height="1"/><rect fill="#bf64cc" x="17" y="9" width="1" height="1"/><rect fill="#bf64cc" x="18" y="9" width="1" height="1"/><rect fill="#bf64cc" x="19" y="9" width="1" height="1"/><rect fill="#bf64cc" x="20" y="9" width="1" height="1"/><rect fill="#be65cc6a" x="21" y="9" width="1" height="1"/><rect fill="#bf64cc70" x="4" y="10" width="1" height="1"/><rect fill="#bf64cc" x="5" y="10" width="1" height="1"/><rect fill="#bf64cc" x="6" y="10" width="1" height="1"/><rect fill="#fbf5fc" x="7" y="10" width="1" height="1"/><rect fill="#ffffff" x="8" y="10" width="1" height="1"/><rect fill="#e8c6ec" x="9" y="10" width="1" height="1"/><rect fill="#bf64cc" x="10" y="10" width="1" height="1"/><rect fill="#bf64cc" x="11" y="10" width="1" height="1"/><rect fill="#cd86d7" x="12" y="10" width="1" height="1"/><rect fill="#f1def4" x="13" y="10" width="1" height="1"/><rect fill="#ffffff" x="14" y="10" width="1" height="1"/><rect fill="#ffffff" x="15" y="10" width="1" height="1"/><rect fill="#faf2fb" x="16" y="10" width="1" height="1"/><rect fill="#d69ddf" x="17" y="10" width="1" height="1"/><rect fill="#bf64cc" x="18" y="10" width="1" height="1"/><rect fill="#bf64cc" x="19" y="10" width="1" height="1"/><rect fill="#be63cbee" x="20" y="10" width="1" height="1"/><rect fill="#bb66cc0f" x="21" y="10" width="1" height="1"/><rect fill="#be65ca53" x="4" y="11" width="1" height="1"/><rect fill="#bf64cc" x="5" y="11" width="1" height="1"/><rect fill="#bf64cc" x="6" y="11" width="1" height="1"/><rect fill="#f2dff4" x="7" y="11" width="1" height="1"/><rect fill="#ffffff" x="8" y="11" width="1" height="1"/><rect fill="#faf4fb" x="9" y="11" width="1" height="1"/><rect fill="#d8a1e0" x="10" y="11" width="1" height="1"/><rect fill="#e4bfea" x="11" y="11" width="1" height="1"/><rect fill="#fefeff" x="12" y="11" width="1" height="1"/><rect fill="#ffffff" x="13" y="11" width="1" height="1"/><rect fill="#ffffff" x="14" y="11" width="1" height="1"/><rect fill="#ffffff" x="15" y="11" width="1" height="1"/><rect fill="#ffffff" x="16" y="11" width="1" height="1"/><rect fill="#fffeff" x="17" y="11" width="1" height="1"/><rect fill="#d394dc" x="18" y="11" width="1" height="1"/><rect fill="#bf64cc" x="19" y="11" width="1" height="1"/><rect fill="#be63ccf6" x="20" y="11" width="1" height="1"/><rect fill="#c369d211" x="21" y="11" width="1" height="1"/><rect fill="#c369d211" x="4" y="12" width="1" height="1"/><rect fill="#be63ccf6" x="5" y="12" width="1" height="1"/><rect fill="#bf64cc" x="6" y="12" width="1" height="1"/><rect fill="#d394dc" x="7" y="12" width="1" height="1"/><rect fill="#fffeff" x="8" y="12" width="1" height="1"/><rect fill="#ffffff" x="9" y="12" width="1" height="1"/><rect fill="#ffffff" x="10" y="12" width="1" height="1"/><rect fill="#ffffff" x="11" y="12" width="1" height="1"/><rect fill="#ffffff" x="12" y="12" width="1" height="1"/><rect fill="#fefeff" x="13" y="12" width="1" height="1"/><rect fill="#e4bfea" x="14" y="12" width="1" height="1"/><rect fill="#d8a0e0" x="15" y="12" width="1" height="1"/><rect fill="#faf4fb" x="16" y="12" width="1" height="1"/><rect fill="#ffffff" x="17" y="12" width="1" height="1"/><rect fill="#f2dff4" x="18" y="12" width="1" height="1"/><rect fill="#bf64cc" x="19" y="12" width="1" height="1"/><rect fill="#bf64cc" x="20" y="12" width="1" height="1"/><rect fill="#be65ca53" x="21" y="12" width="1" height="1"/><rect fill="#bf5fcf10" x="4" y="13" width="1" height="1"/><rect fill="#be63cbee" x="5" y="13" width="1" height="1"/><rect fill="#bf64cc" x="6" y="13" width="1" height="1"/><rect fill="#bf64cc" x="7" y="13" width="1" height="1"/><rect fill="#d69ddf" x="8" y="13" width="1" height="1"/><rect fill="#faf2fb" x="9" y="13" width="1" height="1"/><rect fill="#ffffff" x="10" y="13" width="1" height="1"/><rect fill="#ffffff" x="11" y="13" width="1" height="1"/><rect fill="#f1def4" x="12" y="13" width="1" height="1"/><rect fill="#cd86d7" x="13" y="13" width="1" height="1"/><rect fill="#bf64cc" x="14" y="13" width="1" height="1"/><rect fill="#bf64cc" x="15" y="13" width="1" height="1"/><rect fill="#e8c6ec" x="16" y="13" width="1" height="1"/><rect fill="#ffffff" x="17" y="13" width="1" height="1"/><rect fill="#fbf5fc" x="18" y="13" width="1" height="1"/><rect fill="#bf64cc" x="19" y="13" width="1" height="1"/><rect fill="#bf64cc" x="20" y="13" width="1" height="1"/><rect fill="#be63cc6e" x="21" y="13" width="1" height="1"/><rect fill="#be64cc6b" x="4" y="14" width="1" height="1"/><rect fill="#bf64cc" x="5" y="14" width="1" height="1"/><rect fill="#bf64cc" x="6" y="14" width="1" height="1"/><rect fill="#bf64cc" x="7" y="14" width="1" height="1"/><rect fill="#bf64cc" x="8" y="14" width="1" height="1"/><rect fill="#c066cd" x="9" y="14" width="1" height="1"/><rect fill="#c97cd4" x="10" y="14" width="1" height="1"/><rect fill="#c674d1" x="11" y="14" width="1" height="1"/><rect fill="#bf64cc" x="12" y="14" width="1" height="1"/><rect fill="#bf64cc" x="13" y="14" width="1" height="1"/><rect fill="#bf65cc" x="14" y="14" width="1" height="1"/><rect fill="#daa5e1" x="15" y="14" width="1" height="1"/><rect fill="#fdfafd" x="16" y="14" width="1" height="1"/><rect fill="#ffffff" x="17" y="14" width="1" height="1"/><rect fill="#f3e1f5" x="18" y="14" width="1" height="1"/><rect fill="#bf64cc" x="19" y="14" width="1" height="1"/><rect fill="#bf64cc" x="20" y="14" width="1" height="1"/><rect fill="#bd64cb5e" x="21" y="14" width="1" height="1"/><rect fill="#be64cba3" x="4" y="15" width="1" height="1"/><rect fill="#bf64cc" x="5" y="15" width="1" height="1"/><rect fill="#c879d3" x="6" y="15" width="1" height="1"/><rect fill="#ecd0f0" x="7" y="15" width="1" height="1"/><rect fill="#c97dd4" x="8" y="15" width="1" height="1"/><rect fill="#bf64cc" x="9" y="15" width="1" height="1"/><rect fill="#bf64cc" x="10" y="15" width="1" height="1"/><rect fill="#bf64cc" x="11" y="15" width="1" height="1"/><rect fill="#bf64cc" x="12" y="15" width="1" height="1"/><rect fill="#ca7ed5" x="13" y="15" width="1" height="1"/><rect fill="#f0dcf3" x="14" y="15" width="1" height="1"/><rect fill="#ffffff" x="15" y="15" width="1" height="1"/><rect fill="#ffffff" x="16" y="15" width="1" height="1"/><rect fill="#fefdfe" x="17" y="15" width="1" height="1"/><rect fill="#d395dc" x="18" y="15" width="1" height="1"/><rect fill="#bf64cc" x="19" y="15" width="1" height="1"/><rect fill="#be64cbfe" x="20" y="15" width="1" height="1"/><rect fill="#bd62cd1f" x="21" y="15" width="1" height="1"/><rect fill="#be63cbae" x="4" y="16" width="1" height="1"/><rect fill="#bf64cc" x="5" y="16" width="1" height="1"/><rect fill="#cb81d6" x="6" y="16" width="1" height="1"/><rect fill="#ffffff" x="7" y="16" width="1" height="1"/><rect fill="#fffeff" x="8" y="16" width="1" height="1"/><rect fill="#d59ade" x="9" y="16" width="1" height="1"/><rect fill="#bf64cc" x="10" y="16" width="1" height="1"/><rect fill="#c168cd" x="11" y="16" width="1" height="1"/><rect fill="#dfb2e6" x="12" y="16" width="1" height="1"/><rect fill="#fdfbfe" x="13" y="16" width="1" height="1"/><rect fill="#ffffff" x="14" y="16" width="1" height="1"/><rect fill="#ffffff" x="15" y="16" width="1" height="1"/><rect fill="#faf3fb" x="16" y="16" width="1" height="1"/><rect fill="#d496dd" x="17" y="16" width="1" height="1"/><rect fill="#bf64cc" x="18" y="16" width="1" height="1"/><rect fill="#bf64cc" x="19" y="16" width="1" height="1"/><rect fill="#be63cca7" x="20" y="16" width="1" height="1"/><rect fill="#bf64cb8c" x="4" y="17" width="1" height="1"/><rect fill="#bf64cc" x="5" y="17" width="1" height="1"/><rect fill="#c36dcf" x="6" y="17" width="1" height="1"/><rect fill="#fbf6fc" x="7" y="17" width="1" height="1"/><rect fill="#ffffff" x="8" y="17" width="1" height="1"/><rect fill="#f7ecf9" x="9" y="17" width="1" height="1"/><rect fill="#deafe5" x="10" y="17" width="1" height="1"/><rect fill="#f5e7f7" x="11" y="17" width="1" height="1"/><rect fill="#ffffff" x="12" y="17" width="1" height="1"/><rect fill="#ffffff" x="13" y="17" width="1" height="1"/><rect fill="#ffffff" x="14" y="17" width="1" height="1"/><rect fill="#e8c8ed" x="15" y="17" width="1" height="1"/><rect fill="#c571d0" x="16" y="17" width="1" height="1"/><rect fill="#bf64cc" x="17" y="17" width="1" height="1"/><rect fill="#bf64cc" x="18" y="17" width="1" height="1"/><rect fill="#bf64cbd5" x="19" y="17" width="1" height="1"/><rect fill="#b863c612" x="20" y="17" width="1" height="1"/><rect fill="#bf66cc3c" x="4" y="18" width="1" height="1"/><rect fill="#bf64cc" x="5" y="18" width="1" height="1"/><rect fill="#bf64cc" x="6" y="18" width="1" height="1"/><rect fill="#ddaee4" x="7" y="18" width="1" height="1"/><rect fill="#ffffff" x="8" y="18" width="1" height="1"/><rect fill="#ffffff" x="9" y="18" width="1" height="1"/><rect fill="#ffffff" x="10" y="18" width="1" height="1"/><rect fill="#ffffff" x="11" y="18" width="1" height="1"/><rect fill="#ffffff" x="12" y="18" width="1" height="1"/><rect fill="#f7ecf9" x="13" y="18" width="1" height="1"/><rect fill="#d190db" x="14" y="18" width="1" height="1"/><rect fill="#bf64cc" x="15" y="18" width="1" height="1"/><rect fill="#bf64cc" x="16" y="18" width="1" height="1"/><rect fill="#bf64cc" x="17" y="18" width="1" height="1"/><rect fill="#bf63cba4" x="18" y="18" width="1" height="1"/><rect fill="#b65bc80e" x="19" y="18" width="1" height="1"/><rect fill="#be64cbb2" x="5" y="19" width="1" height="1"/><rect fill="#bf64cc" x="6" y="19" width="1" height="1"/><rect fill="#bf64cc" x="7" y="19" width="1" height="1"/><rect fill="#daa5e1" x="8" y="19" width="1" height="1"/><rect fill="#f8edf9" x="9" y="19" width="1" height="1"/><rect fill="#ffffff" x="10" y="19" width="1" height="1"/><rect fill="#fbf5fc" x="11" y="19" width="1" height="1"/><rect fill="#e1b5e7" x="12" y="19" width="1" height="1"/><rect fill="#c26bce" x="13" y="19" width="1" height="1"/><rect fill="#bf64cc" x="14" y="19" width="1" height="1"/><rect fill="#bf64cc" x="15" y="19" width="1" height="1"/><rect fill="#be63cbdf" x="16" y="19" width="1" height="1"/><rect fill="#bf63cd48" x="17" y="19" width="1" height="1"/><rect fill="#c369d211" x="5" y="20" width="1" height="1"/><rect fill="#bf64cbd1" x="6" y="20" width="1" height="1"/><rect fill="#bf64cc" x="7" y="20" width="1" height="1"/><rect fill="#bf64cc" x="8" y="20" width="1" height="1"/><rect fill="#bf64cc" x="9" y="20" width="1" height="1"/><rect fill="#c26bce" x="10" y="20" width="1" height="1"/><rect fill="#bf64cc" x="11" y="20" width="1" height="1"/><rect fill="#bf64cc" x="12" y="20" width="1" height="1"/><rect fill="#bf64cc" x="13" y="20" width="1" height="1"/><rect fill="#bf64ccfc" x="14" y="20" width="1" height="1"/><rect fill="#be64cc8e" x="15" y="20" width="1" height="1"/><rect fill="#b95cd00b" x="16" y="20" width="1" height="1"/><rect fill="#bf6ad40c" x="6" y="21" width="1" height="1"/><rect fill="#be64cc93" x="7" y="21" width="1" height="1"/><rect fill="#bf64cbf9" x="8" y="21" width="1" height="1"/><rect fill="#bf64cc" x="9" y="21" width="1" height="1"/><rect fill="#bf64cc" x="10" y="21" width="1" height="1"/><rect fill="#bf64cc" x="11" y="21" width="1" height="1"/><rect fill="#bf64cc" x="12" y="21" width="1" height="1"/><rect fill="#be63cbc3" x="13" y="21" width="1" height="1"/><rect fill="#bf62cd34" x="14" y="21" width="1" height="1"/><rect fill="#c369d211" x="8" y="22" width="1" height="1"/><rect fill="#be65ca53" x="9" y="22" width="1" height="1"/><rect fill="#bf64cc70" x="10" y="22" width="1" height="1"/><rect fill="#bd62ca62" x="11" y="22" width="1" height="1"/><rect fill="#bd64c92b" x="12" y="22" width="1" height="1"/></svg>
\ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..20f8d04 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,11 @@ +<script lang="ts"> + import favicon from '$lib/assets/favicon.svg'; + + let { children } = $props(); +</script> + +<svelte:head> + <link rel="icon" href={favicon} /> +</svelte:head> + +{@render children?.()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..7cc3eb7 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,202 @@ +<!-- +MIT License + +Copyright (c) 2025 memdmp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + --> + +<script lang="ts"> + import Alerts, { alert, confirm } from '$lib/Alerts.svelte'; + import { onMount } from 'svelte'; + + let fileInput = $state(null as unknown as HTMLInputElement); + let fileName = $state(''); + let fileRaw = $state(null as null | File); + let canvas = $state(null as unknown as HTMLCanvasElement); + let imgUrl = $state(''); + let output = $state(''); + $effect(() => { + if (fileInput?.files?.length) { + const file = fileInput.files![0]; + fileName = file.name; + fileRaw = file ?? null; + } + }); + onMount(() => { + const el = document.createElement('canvas'); + const ctx = el.getContext('2d'); + if (!ctx) { + alert( + "Could not create 2d Canvas Context. This application will not work. Make sure any privacy-resistance features are disabled and your browser isn't ancient.", + { + title: 'Fatal Error', + } + ); + } else { + if ( + JSON.stringify(ctx.getImageData(0, 0, 32, 32).data) !== + JSON.stringify(ctx.getImageData(0, 0, 32, 32).data) + ) { + alert( + "Canvas readbacks not identical. This application may not work correctly. Make sure any privacy-resistance features are disabled and your browser isn't ancient.", + { + title: 'Error', + } + ); + } + } + }); + const update = async (fileRaw: File) => { + const blob = new Blob([await fileRaw.bytes()]); + const blobUrl = URL.createObjectURL(blob); + imgUrl = blobUrl; + }; + $effect(() => { + if (fileRaw) update(fileRaw); + else imgUrl = ''; + }); + const toHex = (v: number) => Math.floor(v).toString(16).padStart(2, '0'); + const save = ( + data: [r: number, g: number, b: number, a: number][], + width: number, + height: number + ) => { + let content = `<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${width} ${height}`)} width=${JSON.stringify('' + width)} height=${JSON.stringify('' + height)}>`; + let item: (typeof data)[0]; + let x = 0; + let y = 0; + while ((item = data.shift()!)) { + if (++x >= width) (x = 0), y++; + if (item[3] !== 0) + content += `<rect fill="#${toHex(item[0])}${toHex(item[1])}${toHex(item[2])}${item[3] !== 255 ? toHex(item[3]) : ''}" x=${JSON.stringify('' + x)} y=${JSON.stringify('' + y)} width="1" height="1"/>`; + } + content += '</svg>'; + output = URL.createObjectURL( + new Blob([content], { + type: 'image/svg+xml', + }) + ); + }; +</script> + +<svelte:head> + <title>Bitmap to Pixelated SVG</title> + <link rel="stylesheet" href="/cs16.css" /> +</svelte:head> + +<Alerts /> +<div> + <h1>PixelSVG</h1> + <div> + <label style="display: flex; gap:4px;"> + <span class="cs-btn" + >{fileName ? `File: ${fileName}` : 'Upload a File'}</span + > + <img + src={imgUrl} + alt="" + style="height: 24px" + onload={async (e) => { + const imgEl = e.currentTarget as HTMLImageElement; + const rect = { + width: imgEl.naturalWidth, + height: imgEl.naturalHeight, + }; + if ( + rect.width * rect.height > 65536 && + !(await confirm( + `This image isn't small; it's made up of ${rect.width * rect.height} pixels. The outputted file could be up to ${((`<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges" viewBox=${JSON.stringify(`0 0 ${rect.width} ${rect.height}`)} width=${JSON.stringify('' + rect.width)} height=${JSON.stringify('' + rect.height)}></svg>`.length + `<rect fill="#ffffffff" x="${rect.width}" y="${rect.height}" width="1" height="1"/>`.length * rect.width * rect.height) / 1024 / 1024).toFixed(1)} MB large. It may crash your browser. Are you sure you wish to continue?`, + { + title: 'Confirmation', + } + )) + ) + return (imgUrl = ''), (fileName = ''); + canvas.width = rect.width; + canvas.height = rect.height; + const ctx = canvas.getContext('2d', { + alpha: true, + colorSpace: 'display-p3', + }); + if (!ctx) throw new Error('no 2d context obtainable'); + try { + ctx.drawImage(e.currentTarget as HTMLImageElement, 0, 0); + } catch (error) { + await alert( + "Failed to draw image to canvas. The image's data may be invalid or the image may be too big. Check the Console.", + { + title: 'Failed to draw image', + } + ); + throw error; + } + const data = ctx.getImageData(0, 0, rect.width, rect.height, { + colorSpace: 'display-p3', + }); + let out = [] as [r: number, g: number, b: number, a: number][]; + for (let i = 0; i < data.data.length; i += 4) { + const r = data.data[i + 0]; + const g = data.data[i + 1]; + const b = data.data[i + 2]; + const a = data.data[i + 3]; + out.push([r, g, b, a]); + } + save(out, rect.width, rect.height); + }} + onerror={(e) => { + if (imgUrl !== '') + alert( + 'Failed to load the image into an HTML Image Element. Please make sure the image you provided is valid and in a format your browser understands.', + { + title: 'Failed to load image', + } + ); + }} + /> + <input + type="file" + bind:this={fileInput} + onchange={() => { + if (fileInput?.files?.length) { + const file = fileInput.files![0]; + fileName = file.name; + fileRaw = file ?? null; + } + }} + style="opacity:0;position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;" + /> + </label> + </div> + {#if output} + <div style="margin-top:12px;margin-bottom:12px;"> + Output:<br /> + <a href={output} target="_blank" + ><img src={output} alt="could not render" /> (open)</a + > + </div> + {/if} + <div style="opacity:0.5"> + {imgUrl ? 'Canvas:' : ''}<br /><canvas + bind:this={canvas} + width="0" + height="0" + ></canvas> + </div> +</div> |