Enhance sprite-picker

This commit is contained in:
2025-03-11 17:40:01 -05:00
parent 2c06ae78b4
commit c148272d38
9 changed files with 7522 additions and 29 deletions

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<link rel="stylesheet" type="text/css" href="https://alttpr.com/css/app.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>Vite App</title>
</head> </head>

12
package-lock.json generated
View File

@@ -18,7 +18,8 @@
"localforage": "^1.10.0", "localforage": "^1.10.0",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"vue-select": "^4.0.0-beta.6"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.1", "@modyfi/vite-plugin-yaml": "^1.1.1",
@@ -3143,6 +3144,15 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue-select": {
"version": "4.0.0-beta.6",
"resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz",
"integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==",
"license": "MIT",
"peerDependencies": {
"vue": "3.x"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -19,7 +19,8 @@
"localforage": "^1.10.0", "localforage": "^1.10.0",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"vue-select": "^4.0.0-beta.6"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.1", "@modyfi/vite-plugin-yaml": "^1.1.1",

View File

@@ -12,8 +12,8 @@ export default class ZSPR {
} }
_parse() { _parse() {
this.gfxOffset = (this.patch[12] << 24) | (this.patch[11] << 16) | (this.patch[10] << 8) | this.patch[9]; this.gfxOffset = ((this.patch[12] << 24) | (this.patch[11] << 16) | (this.patch[10] << 8) | this.patch[9]) >>> 0;
this.palOffset = (this.patch[18] << 24) | (this.patch[17] << 16) | (this.patch[16] << 8) | this.patch[15]; this.palOffset = ((this.patch[18] << 24) | (this.patch[17] << 16) | (this.patch[16] << 8) | this.patch[15]) >>> 0;
const metadataOffset = 0x1D; const metadataOffset = 0x1D;
const dec8 = new TextDecoder("utf-8"); const dec8 = new TextDecoder("utf-8");
const dec16 = new TextDecoder("utf-16"); const dec16 = new TextDecoder("utf-16");

View File

@@ -26,3 +26,13 @@
--bs-btn-color: #880; --bs-btn-color: #880;
--bs-btn-active-color: #550; --bs-btn-active-color: #550;
} }
.sprite-preview-icon {
display: inline;
float: left;
padding-right: 0.7em;
}
.sprite-preview-name {
padding-left: 0.7em;
}

View File

@@ -33,7 +33,6 @@ export default defineComponent({
rom_checksum: "3322EFFC", rom_checksum: "3322EFFC",
baserom: null, baserom: null,
baserom_error: null, baserom_error: null,
sprite: null,
patch: null, patch: null,
error: null, error: null,
settings: null, settings: null,
@@ -54,7 +53,6 @@ export default defineComponent({
} }
await this.fetchSeed(); await this.fetchSeed();
}, },
computed: { computed: {
permalink() { permalink() {
@@ -121,15 +119,10 @@ export default defineComponent({
}.bind(this); }.bind(this);
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}, },
spriteUpdate(sprite) {
this.sprite = sprite;
},
async patchRom() { async patchRom() {
var rom = bps.apply(this.patch, this.baserom); var rom = bps.apply(this.patch, this.baserom);
if (this.sprite) { await this.$refs.spritepicker.patch(rom);
this.sprite.apply(rom);
}
this.$refs.heartbeep.patch(rom); this.$refs.heartbeep.patch(rom);
this.$refs.heartcolor.patch(rom); this.$refs.heartcolor.patch(rom);
@@ -187,7 +180,7 @@ export default defineComponent({
</li> </li>
<li class="list-group-item"> <li class="list-group-item">
<div class="mb-2"> <div class="mb-2">
<SpritePicker @spriteUpdate="spriteUpdate" /> <SpritePicker ref="spritepicker" />
</div> </div>
</li> </li>
<li class="list-group-item"> <li class="list-group-item">

View File

@@ -3,59 +3,160 @@ import { defineComponent } from 'vue';
import ZSPR from "@/ZSPR.js"; import ZSPR from "@/ZSPR.js";
import localforage from "localforage"; import localforage from "localforage";
import officialSprites from "@/data/sprites.json";
import axios from "axios";
import vSelect from "vue-select";
export default defineComponent({ export default defineComponent({
components: {
vSelect,
},
data() { data() {
return { return {
sprite: null, uploadedZspr: null,
sprite_error: null, sprite_error: null,
spriteType: null,
officialSprite: null,
}; };
}, },
async mounted() { async mounted() {
const spriteType = await localforage.getItem("selected_sprite_type") ?? "default";
this.officialSprite = await localforage.getItem("selected_official_sprite");
const uploaded = await localforage.getItem("uploaded_sprite");
if (uploaded) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(uploaded);
document.getElementById("sprite-input").files = dataTransfer.files;
await this.uploadSprite(uploaded, false);
}
this.spriteType = spriteType;
},
watch: {
async spriteType(newValue, oldValue) {
await localforage.setItem("selected_sprite_type", newValue);
},
async officialSprite(newValue, oldValue) {
if (newValue) {
this.spriteType = "official";
} else {
this.spriteType = "default";
}
await localforage.setItem("selected_official_sprite", JSON.parse(JSON.stringify(newValue)));
},
}, },
computed: { computed: {
spriteList() {
return officialSprites;
},
}, },
methods: { methods: {
uploadSprite(file) { async patch(rom) {
if (this.spriteType == "custom" && this.uploadedZspr != null) {
this.uploadedZspr.apply(rom);
} else if (this.spriteType == "official" && this.officialSprite
&& this.officialSprite.file) {
await axios.get(this.officialSprite.file, { responseType: "arraybuffer" })
.then(response => {
const buffer = new Uint8Array(response.data);
const zspr = new ZSPR(buffer);
if (!zspr.valid) {
throw "Downloaded sprite invalid";
return;
}
zspr.apply(rom);
})
.catch(error => {
console.log(error);
});
}
},
uploadSprite(file, updateType) {
if (!file) { if (!file) {
this.sprite_error = null; this.sprite_error = null;
this.sprite = null; this.uploadedZspr = null;
if (updateType) {
this.spriteType = "default";
}
return; return;
} }
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function() { reader.onload = async function() {
const buffer = new Uint8Array(reader.result); const buffer = new Uint8Array(reader.result);
const zspr = new ZSPR(buffer); const zspr = new ZSPR(buffer);
if (!zspr.valid) { if (!zspr.valid) {
this.sprite_error = "Invalid sprite file"; this.sprite_error = "Invalid sprite file";
this.sprite = null; this.uploadedZspr = null;
if (updateType) {
this.spriteType = "default";
}
return; return;
} }
this.sprite_error = null; this.sprite_error = null;
this.sprite = zspr; this.uploadedZspr = zspr;
localforage.setItem("sprite", file); if (updateType) {
this.$emit("spriteUpdate", this.sprite); this.spriteType = "custom";
}
await localforage.setItem("uploaded_sprite", file);
}.bind(this); }.bind(this);
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
}, },
} getSpriteClass(name) {
return "icon-custom-" + name.replace(/\W/g, "");
},
},
}); });
</script> </script>
<template> <template>
<div> <div>
<label for="sprite-input" class="form-label"> <div class="input-group mt-2">
Custom Sprite: <div class="input-group-text">
<template v-if="sprite"> <input name="spriteType" v-model="spriteType" value="default" class="mt-0" type="radio">
{{ sprite.spriteName }} by {{ sprite.spriteAuthor }} </div>
</template> <label class="input-group-text">
</label> Default Link Sprite
<input id="sprite-input" class="form-control" type="file" accept=".zspr" @change="uploadSprite($event.target.files[0])" /> </label>
</div>
<div class="input-group mt-2">
<div class="input-group-text">
<input name="spriteType" v-model="spriteType" value="official" class="mt-0" type="radio">
</div>
<label class="input-group-text" for="spriteSelector">
Sprite:
</label>
<vSelect label="name" :options="spriteList" class="form-control"
id="spriteSelector" v-model="officialSprite">
<template #option="option">
<div class="icon-custom sprite-preview-icon" :class="getSpriteClass(option.name)"></div>
<span class="sprite-preview-name">{{ option.name }}</span>
</template>
<template #selected-option="option">
<div class="icon-custom sprite-preview-icon" :class="getSpriteClass(option.name)"></div>
<span class="sprite-preview-name">{{ option.name }}</span>
</template>
</vSelect>
</div>
<div class="input-group mt-2">
<div class="input-group-text">
<input name="spriteType" v-model="spriteType" value="custom" class="mt-0" type="radio">
</div>
<input id="sprite-input" class="form-control" type="file" accept=".zspr" @change="uploadSprite($event.target.files[0], true)" />
</div>
<div v-if="sprite_error" class="invalid"> <div v-if="sprite_error" class="invalid">
{{ sprite_error }} {{ sprite_error }}
</div> </div>
</div> </div>
</template> </template>
<style>
#spriteSelector {
padding: 0;
--vs-border-style: none;
}
</style>

7376
src/data/sprites.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import router from "./router";
import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import "vue-select/dist/vue-select.css";
import "bootstrap"; import "bootstrap";
import "./assets/main.css"; import "./assets/main.css";