feat(assets): use single texture atlas instead of loading all images individually
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# generated
|
# generated
|
||||||
public/nds/images/projects/pokemons
|
public/nds/images/projects/pokemons
|
||||||
|
public/nds/atlas.webp
|
||||||
app/composables/useAssets.ts
|
app/composables/useAssets.ts
|
||||||
|
|
||||||
# ESlint
|
# ESlint
|
||||||
|
|||||||
@@ -13,29 +13,17 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
const BAR_WIDTH = 256;
|
|
||||||
const BAR_HEIGHT = 24;
|
|
||||||
const TITLE_Y = 5;
|
const TITLE_Y = 5;
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.globalAlpha = props.opacity;
|
ctx.globalAlpha = props.opacity;
|
||||||
|
|
||||||
// top bar
|
// top bar
|
||||||
ctx.drawImage(
|
assets.images.common.barsSheet.draw(ctx, 0, -props.yOffset, {
|
||||||
assets.common.barsSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
(app.color.row * 4 + app.color.col) * BAR_HEIGHT,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
0,
|
|
||||||
-props.yOffset,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.title) {
|
if (props.title) {
|
||||||
ctx.fillStyle = "#000000";
|
ctx.fillStyle = "#000000";
|
||||||
@@ -46,17 +34,9 @@ onRender((ctx) => {
|
|||||||
ctx.scale(1, -1);
|
ctx.scale(1, -1);
|
||||||
|
|
||||||
// bottom bar
|
// bottom bar
|
||||||
ctx.drawImage(
|
assets.images.common.barsSheet.draw(ctx, 0, -192 - props.yOffset, {
|
||||||
assets.common.barsSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
(app.color.row * 4 + app.color.col) * BAR_HEIGHT,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
0,
|
|
||||||
-192 - props.yOffset,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
);
|
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
const ANIMATION_SPEED = 15;
|
const ANIMATION_SPEED = 15;
|
||||||
const CORNER_SIZE = 11;
|
|
||||||
|
|
||||||
let [currentX, currentY, currentWidth, currentHeight] = props.rect;
|
let [currentX, currentY, currentWidth, currentHeight] = props.rect;
|
||||||
|
|
||||||
@@ -48,67 +46,31 @@ onRender((ctx, deltaTime) => {
|
|||||||
|
|
||||||
ctx.globalAlpha = props.opacity;
|
ctx.globalAlpha = props.opacity;
|
||||||
|
|
||||||
const spriteY = (app.color.row * 4 + app.color.col) * CORNER_SIZE;
|
// top-left corner
|
||||||
|
assets.images.common.selectorCornersSheet.draw(ctx, x, y, { colored: true });
|
||||||
|
|
||||||
// Top-left corner
|
// top-right corner
|
||||||
ctx.drawImage(
|
|
||||||
assets.common.selectorCornersSheet,
|
|
||||||
0,
|
|
||||||
spriteY,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Top-right corner
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.scale(-1, 1);
|
ctx.scale(-1, 1);
|
||||||
ctx.drawImage(
|
assets.images.common.selectorCornersSheet.draw(ctx, -(x + w), y, {
|
||||||
assets.common.selectorCornersSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
spriteY,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
-(x + w),
|
|
||||||
y,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// Bottom-left corner
|
// bottom-left corner
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.scale(1, -1);
|
ctx.scale(1, -1);
|
||||||
ctx.drawImage(
|
assets.images.common.selectorCornersSheet.draw(ctx, x, -(y + h), {
|
||||||
assets.common.selectorCornersSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
spriteY,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
x,
|
|
||||||
-(y + h),
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// Bottom-right corner
|
// bottom-right corner
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.scale(-1, -1);
|
ctx.scale(-1, -1);
|
||||||
ctx.drawImage(
|
assets.images.common.selectorCornersSheet.draw(ctx, -(x + w), -(y + h), {
|
||||||
assets.common.selectorCornersSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
spriteY,
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
-(x + w),
|
|
||||||
-(y + h),
|
|
||||||
CORNER_SIZE,
|
|
||||||
CORNER_SIZE,
|
|
||||||
);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}, 40);
|
}, 40);
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const { onRender, onClick } = useScreen();
|
|||||||
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
const BUTTON_WIDTH = assets.common.button.width;
|
const BUTTON_WIDTH = assets.images.common.button.rect.width;
|
||||||
const BUTTON_HEIGHT = assets.common.button.height;
|
const BUTTON_HEIGHT = assets.images.common.button.rect.height;
|
||||||
const LETTER_WIDTH = assets.common.B.width;
|
const LETTER_WIDTH = assets.images.common.B.rect.width;
|
||||||
|
|
||||||
const B_BUTTON: Rect = [31, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
|
const B_BUTTON: Rect = [31, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
|
||||||
const A_BUTTON: Rect = [144, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
|
const A_BUTTON: Rect = [144, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
|
||||||
@@ -29,23 +29,25 @@ onRender((ctx) => {
|
|||||||
ctx.translate(0, props.yOffset);
|
ctx.translate(0, props.yOffset);
|
||||||
|
|
||||||
const drawButton = (
|
const drawButton = (
|
||||||
image: HTMLImageElement,
|
image: {
|
||||||
|
draw: (ctx: CanvasRenderingContext2D, x: number, y: number) => void;
|
||||||
|
},
|
||||||
text: string,
|
text: string,
|
||||||
x: number,
|
x: number,
|
||||||
offset: number,
|
offset: number,
|
||||||
) => {
|
) => {
|
||||||
ctx.drawImage(assets.common.button, x, 172);
|
assets.images.common.button.draw(ctx, x, 172);
|
||||||
|
|
||||||
const { actualBoundingBoxRight: textWidth } = ctx.measureText(text);
|
const { actualBoundingBoxRight: textWidth } = ctx.measureText(text);
|
||||||
const width = LETTER_WIDTH + 4 + textWidth;
|
const width = LETTER_WIDTH + 4 + textWidth;
|
||||||
const left = Math.ceil(x + BUTTON_WIDTH / 2 - width / 2 - offset / 2);
|
const left = Math.ceil(x + BUTTON_WIDTH / 2 - width / 2 - offset / 2);
|
||||||
|
|
||||||
ctx.drawImage(image, left, 176);
|
image.draw(ctx, left, 176);
|
||||||
ctx.fillText(text, left + LETTER_WIDTH + 4, 185);
|
ctx.fillText(text, left + LETTER_WIDTH + 4, 185);
|
||||||
};
|
};
|
||||||
|
|
||||||
drawButton(assets.common.B, props.bLabel, 31, 5);
|
drawButton(assets.images.common.B, props.bLabel, 31, 5);
|
||||||
drawButton(assets.common.A, props.aLabel, 144, 0);
|
drawButton(assets.images.common.A, props.aLabel, 144, 0);
|
||||||
}, 60);
|
}, 60);
|
||||||
|
|
||||||
onClick((x, y) => {
|
onClick((x, y) => {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ const { onRender } = useScreen();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
const confirmationModal = useConfirmationModal();
|
const confirmationModal = useConfirmationModal();
|
||||||
|
|
||||||
const BG_WIDTH = assets.common.confirmationModal.width;
|
const BG_WIDTH = assets.images.common.confirmationModal.rect.width;
|
||||||
const BG_HEIGHT = assets.common.confirmationModal.height;
|
const BG_HEIGHT = assets.images.common.confirmationModal.rect.height;
|
||||||
const BG_X = Math.floor((SCREEN_WIDTH - BG_WIDTH) / 2);
|
const BG_X = Math.floor((SCREEN_WIDTH - BG_WIDTH) / 2);
|
||||||
const BG_Y = Math.floor((SCREEN_HEIGHT - BG_HEIGHT) / 2);
|
const BG_Y = Math.floor((SCREEN_HEIGHT - BG_HEIGHT) / 2);
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
ctx.translate(0, confirmationModal.offsetY);
|
ctx.translate(0, confirmationModal.offsetY);
|
||||||
|
|
||||||
ctx.drawImage(assets.common.confirmationModal, BG_X, BG_Y);
|
assets.images.common.confirmationModal.draw(ctx, BG_X, BG_Y);
|
||||||
|
|
||||||
ctx.font = "16px Pokemon DP Pro";
|
ctx.font = "16px Pokemon DP Pro";
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ const store = useContactStore();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
|
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage2Opacity
|
? store.intro.stage2Opacity
|
||||||
: store.outro.stage3Opacity;
|
: store.outro.stage3Opacity;
|
||||||
|
|
||||||
ctx.drawImage(assets.contact.bottomScreen.background, 0, 0);
|
assets.images.contact.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ onRender((ctx) => {
|
|||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage3Opacity
|
? store.intro.stage3Opacity
|
||||||
: store.outro.stage1Opacity;
|
: store.outro.stage1Opacity;
|
||||||
ctx.drawImage(assets.contact.bottomScreen.buttons, 31, 32);
|
assets.images.contact.bottomScreen.buttons.draw(ctx, 31, 32);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ const store = useContactStore();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.topScreen.background, 0, 0);
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage2Opacity
|
? store.intro.stage2Opacity
|
||||||
: store.outro.stage3Opacity;
|
: store.outro.stage3Opacity;
|
||||||
|
|
||||||
ctx.drawImage(assets.contact.topScreen.background, 0, 0);
|
assets.images.contact.topScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ onRender((ctx) => {
|
|||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage1Opacity
|
? store.intro.stage1Opacity
|
||||||
: store.outro.stage2Opacity;
|
: store.outro.stage2Opacity;
|
||||||
ctx.drawImage(assets.contact.topScreen.leftBar, 0, 0);
|
assets.images.contact.topScreen.leftBar.draw(ctx, 0, 0);
|
||||||
|
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage3Opacity
|
? store.intro.stage3Opacity
|
||||||
: store.outro.stage1Opacity;
|
: store.outro.stage1Opacity;
|
||||||
ctx.drawImage(assets.contact.topScreen.leftBarThings, 0, 0);
|
assets.images.contact.topScreen.leftBarThings.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ onRender((ctx) => {
|
|||||||
const y = 169 - 24 * index + store.notificationsYOffset;
|
const y = 169 - 24 * index + store.notificationsYOffset;
|
||||||
if (y < -24) break;
|
if (y < -24) break;
|
||||||
|
|
||||||
ctx.drawImage(assets.contact.bottomScreen.notification, 21, y);
|
assets.images.contact.bottomScreen.notification.draw(ctx, 21, y);
|
||||||
|
|
||||||
const content = store.notifications[i]!;
|
const content = store.notifications[i]!;
|
||||||
ctx.fillStyle = content.includes("opened") ? "#00fbba" : "#e3f300";
|
ctx.fillStyle = content.includes("opened") ? "#00fbba" : "#e3f300";
|
||||||
@@ -26,8 +26,8 @@ onRender((ctx) => {
|
|||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage1Opacity
|
? store.intro.stage1Opacity
|
||||||
: store.outro.stage2Opacity;
|
: store.outro.stage2Opacity;
|
||||||
ctx.drawImage(
|
assets.images.contact.topScreen.title.draw(
|
||||||
assets.contact.topScreen.title,
|
ctx,
|
||||||
21,
|
21,
|
||||||
store.isIntro
|
store.isIntro
|
||||||
? store.intro.titleY
|
? store.intro.titleY
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const store = useGalleryStore();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
|
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
|
|
||||||
ctx.fillStyle = "#000000";
|
ctx.fillStyle = "#000000";
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.topScreen.background, 0, 0);
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
||||||
|
|
||||||
ctx.fillStyle = "#000000";
|
ctx.fillStyle = "#000000";
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ const { onRender } = useScreen();
|
|||||||
|
|
||||||
const store = useHomeStore();
|
const store = useHomeStore();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
||||||
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
|
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ const props = defineProps<{
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
image: HTMLImageElement;
|
image: {
|
||||||
|
draw: (ctx: CanvasRenderingContext2D, x: number, y: number) => void;
|
||||||
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.globalAlpha = props.opacity;
|
ctx.globalAlpha = props.opacity;
|
||||||
ctx.drawImage(props.image, props.x, props.y);
|
props.image.draw(ctx, props.x, props.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -128,45 +128,45 @@ onRender((ctx) => {
|
|||||||
:x="33"
|
:x="33"
|
||||||
:y="25 + getButtonOffset('projects')"
|
:y="25 + getButtonOffset('projects')"
|
||||||
:opacity="getOpacity('projects')"
|
:opacity="getOpacity('projects')"
|
||||||
:image="assets.home.bottomScreen.buttons.game"
|
:image="assets.images.home.bottomScreen.buttons.game"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:x="32"
|
:x="32"
|
||||||
:y="72 + getButtonOffset('contact')"
|
:y="72 + getButtonOffset('contact')"
|
||||||
:opacity="getOpacity('contact')"
|
:opacity="getOpacity('contact')"
|
||||||
:image="assets.home.bottomScreen.buttons.contact"
|
:image="assets.images.home.bottomScreen.buttons.contact"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:x="128"
|
:x="128"
|
||||||
:y="72 + getButtonOffset('gallery')"
|
:y="72 + getButtonOffset('gallery')"
|
||||||
:opacity="getOpacity('gallery')"
|
:opacity="getOpacity('gallery')"
|
||||||
:image="assets.home.bottomScreen.buttons.gallery"
|
:image="assets.images.home.bottomScreen.buttons.gallery"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
:x="33"
|
:x="33"
|
||||||
:y="121"
|
:y="121"
|
||||||
:opacity="getOpacity()"
|
:opacity="getOpacity()"
|
||||||
:image="assets.home.bottomScreen.buttons.gamePak"
|
:image="assets.images.home.bottomScreen.buttons.gamePak"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
:x="10"
|
:x="10"
|
||||||
:y="175 + getButtonOffset('theme')"
|
:y="175 + getButtonOffset('theme')"
|
||||||
:opacity="getOpacity('theme')"
|
:opacity="getOpacity('theme')"
|
||||||
:image="assets.home.bottomScreen.buttons.theme"
|
:image="assets.images.home.bottomScreen.buttons.theme"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:x="117"
|
:x="117"
|
||||||
:y="170 + getButtonOffset('settings')"
|
:y="170 + getButtonOffset('settings')"
|
||||||
:opacity="getOpacity('settings')"
|
:opacity="getOpacity('settings')"
|
||||||
:image="assets.home.bottomScreen.buttons.settings"
|
:image="assets.images.home.bottomScreen.buttons.settings"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
:x="235"
|
:x="235"
|
||||||
:y="175 + getButtonOffset('alarm')"
|
:y="175 + getButtonOffset('alarm')"
|
||||||
:opacity="getOpacity('alarm')"
|
:opacity="getOpacity('alarm')"
|
||||||
:image="assets.home.bottomScreen.buttons.alarm"
|
:image="assets.images.home.bottomScreen.buttons.alarm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Selector :rect="selectorPosition" :opacity="getOpacity()" />
|
<Selector :rect="selectorPosition" :opacity="getOpacity()" />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { assets } = useAssets();
|
|||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
|
||||||
ctx.drawImage(assets.home.topScreen.background, 0, 0);
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
// NOTE: calendar background is handled by TopScreenBackground
|
// NOTE: calendar background is handled by TopScreenBackground
|
||||||
const app = useAppStore();
|
|
||||||
const store = useHomeStore();
|
const store = useHomeStore();
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ onRender((ctx) => {
|
|||||||
const CALENDAR_ROWS = 5;
|
const CALENDAR_ROWS = 5;
|
||||||
const CALENDAR_LEFT = 128;
|
const CALENDAR_LEFT = 128;
|
||||||
const CALENDAR_TOP = 64;
|
const CALENDAR_TOP = 64;
|
||||||
const SELECTOR_SIZE = 13;
|
|
||||||
|
|
||||||
ctx.fillStyle = "#343434";
|
ctx.fillStyle = "#343434";
|
||||||
|
|
||||||
@@ -30,16 +28,16 @@ onRender((ctx) => {
|
|||||||
: store.isOutro && store.outro.animateTop
|
: store.isOutro && store.outro.animateTop
|
||||||
? store.outro.stage1Opacity
|
? store.outro.stage1Opacity
|
||||||
: 1;
|
: 1;
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.calendar.draw(
|
||||||
assets.home.topScreen.calendar.calendar,
|
ctx,
|
||||||
CALENDAR_LEFT - 3,
|
CALENDAR_LEFT - 3,
|
||||||
CALENDAR_TOP - 33,
|
CALENDAR_TOP - 33,
|
||||||
);
|
);
|
||||||
|
|
||||||
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
||||||
if (extraRow) {
|
if (extraRow) {
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.lastRow.draw(
|
||||||
assets.home.topScreen.calendar.lastRow,
|
ctx,
|
||||||
CALENDAR_LEFT - 3,
|
CALENDAR_LEFT - 3,
|
||||||
CALENDAR_TOP + 79,
|
CALENDAR_TOP + 79,
|
||||||
);
|
);
|
||||||
@@ -63,16 +61,11 @@ onRender((ctx) => {
|
|||||||
const cellTop = CALENDAR_TOP + col * 16;
|
const cellTop = CALENDAR_TOP + col * 16;
|
||||||
|
|
||||||
if (now.getDate() === day) {
|
if (now.getDate() === day) {
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.daySelectorsSheet.draw(
|
||||||
assets.home.topScreen.calendar.daySelectorsSheet,
|
ctx,
|
||||||
0,
|
|
||||||
(app.color.row * 4 + app.color.col) * SELECTOR_SIZE,
|
|
||||||
SELECTOR_SIZE,
|
|
||||||
SELECTOR_SIZE,
|
|
||||||
cellLeft + 1,
|
cellLeft + 1,
|
||||||
cellTop + 1,
|
cellTop + 1,
|
||||||
SELECTOR_SIZE,
|
{ colored: true },
|
||||||
SELECTOR_SIZE,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ onRender((ctx) => {
|
|||||||
: store.isOutro && store.outro.animateTop
|
: store.isOutro && store.outro.animateTop
|
||||||
? store.outro.stage1Opacity
|
? store.outro.stage1Opacity
|
||||||
: 1;
|
: 1;
|
||||||
ctx.drawImage(assets.home.topScreen.clock, 13, 45);
|
assets.images.home.topScreen.clock.draw(ctx, 13, 45);
|
||||||
|
|
||||||
ctx.globalAlpha = store.isIntro
|
ctx.globalAlpha = store.isIntro
|
||||||
? store.intro.stage1Opacity
|
? store.intro.stage1Opacity
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const store = useHomeStore();
|
const store = useHomeStore();
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
const BAR_WIDTH = 256;
|
|
||||||
const BAR_HEIGHT = 16;
|
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
const TEXT_Y = 11;
|
const TEXT_Y = 11;
|
||||||
|
|
||||||
@@ -15,17 +11,9 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
ctx.globalAlpha =
|
ctx.globalAlpha =
|
||||||
store.isOutro && store.outro.animateTop ? store.outro.stage2Opacity : 1;
|
store.isOutro && store.outro.animateTop ? store.outro.stage2Opacity : 1;
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.statusBar.statusBarsSheet.draw(ctx, 0, 0, {
|
||||||
assets.home.topScreen.statusBar.statusBarsSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
(app.color.row * 4 + app.color.col) * BAR_HEIGHT,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.font = "7px NDS7";
|
ctx.font = "7px NDS7";
|
||||||
@@ -55,9 +43,9 @@ onRender((ctx) => {
|
|||||||
fillNumberCell(now.getMonth() + 1, 12, -1);
|
fillNumberCell(now.getMonth() + 1, 12, -1);
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.gbaDisplay, 210, 2);
|
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.startupMode, 226, 2);
|
assets.images.home.topScreen.statusBar.startupMode.draw(ctx, 226, 2);
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.battery, 242, 4);
|
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const props = defineProps<{
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
const model = assets.nitendoDs.model.clone(true);
|
const model = assets.models.nitendoDs.model.clone(true);
|
||||||
|
|
||||||
let topScreenTexture: THREE.CanvasTexture | null = null;
|
let topScreenTexture: THREE.CanvasTexture | null = null;
|
||||||
let bottomScreenTexture: THREE.CanvasTexture | null = null;
|
let bottomScreenTexture: THREE.CanvasTexture | null = null;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { onRender } = useScreen();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.projects.bottomScreen.background, 0, 0);
|
assets.images.projects.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
defineOptions({
|
defineOptions({
|
||||||
render: () => null,
|
render: () => null,
|
||||||
|
|||||||
@@ -11,23 +11,28 @@ const CLICK_RADIUS = 22;
|
|||||||
const BUTTONS = {
|
const BUTTONS = {
|
||||||
prev: {
|
prev: {
|
||||||
position: [36, 100],
|
position: [36, 100],
|
||||||
image: assets.projects.bottomScreen.prevPressed,
|
image: assets.images.projects.bottomScreen.prevPressed,
|
||||||
},
|
},
|
||||||
quit: {
|
quit: {
|
||||||
position: [88, 156],
|
position: [88, 156],
|
||||||
image: assets.projects.bottomScreen.quitPressed,
|
image: assets.images.projects.bottomScreen.quitPressed,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
position: [168, 156],
|
position: [168, 156],
|
||||||
image: assets.projects.bottomScreen.linkPressed,
|
image: assets.images.projects.bottomScreen.linkPressed,
|
||||||
},
|
},
|
||||||
next: {
|
next: {
|
||||||
position: [220, 100],
|
position: [220, 100],
|
||||||
image: assets.projects.bottomScreen.nextPressed,
|
image: assets.images.projects.bottomScreen.nextPressed,
|
||||||
},
|
},
|
||||||
} as const satisfies Record<
|
} as const satisfies Record<
|
||||||
string,
|
string,
|
||||||
{ position: Point; image: HTMLImageElement }
|
{
|
||||||
|
position: Point;
|
||||||
|
image: {
|
||||||
|
draw: (ctx: CanvasRenderingContext2D, x: number, y: number) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type ButtonType = keyof typeof BUTTONS;
|
type ButtonType = keyof typeof BUTTONS;
|
||||||
@@ -115,15 +120,15 @@ onClick((x, y) => {
|
|||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
// Draw disabled buttons
|
// Draw disabled buttons
|
||||||
if (store.currentProject === 0) {
|
if (store.currentProject === 0) {
|
||||||
ctx.drawImage(
|
assets.images.projects.bottomScreen.prevDisabled.draw(
|
||||||
assets.projects.bottomScreen.prevDisabled,
|
ctx,
|
||||||
BUTTONS.prev.position[0] - 14,
|
BUTTONS.prev.position[0] - 14,
|
||||||
BUTTONS.prev.position[1] - 14,
|
BUTTONS.prev.position[1] - 14,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (store.currentProject === store.projects.length - 1) {
|
if (store.currentProject === store.projects.length - 1) {
|
||||||
ctx.drawImage(
|
assets.images.projects.bottomScreen.nextDisabled.draw(
|
||||||
assets.projects.bottomScreen.nextDisabled,
|
ctx,
|
||||||
BUTTONS.next.position[0] - 14,
|
BUTTONS.next.position[0] - 14,
|
||||||
BUTTONS.next.position[1] - 14,
|
BUTTONS.next.position[1] - 14,
|
||||||
);
|
);
|
||||||
@@ -131,24 +136,24 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
if (currentAnimation?.showButton) {
|
if (currentAnimation?.showButton) {
|
||||||
const image = BUTTONS[currentAnimation.type].image;
|
const image = BUTTONS[currentAnimation.type].image;
|
||||||
ctx.drawImage(
|
image.draw(
|
||||||
image!,
|
ctx,
|
||||||
currentAnimation.position[0] - 14,
|
currentAnimation.position[0] - 14,
|
||||||
currentAnimation.position[1] - 14,
|
currentAnimation.position[1] - 14,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentAnimation?.showSmallCircle) {
|
if (currentAnimation?.showSmallCircle) {
|
||||||
ctx.drawImage(
|
assets.images.projects.bottomScreen.circleSmall.draw(
|
||||||
assets.projects.bottomScreen.circleSmall,
|
ctx,
|
||||||
currentAnimation.position[0] - 28,
|
currentAnimation.position[0] - 28,
|
||||||
currentAnimation.position[1] - 28,
|
currentAnimation.position[1] - 28,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentAnimation?.showBigCircle) {
|
if (currentAnimation?.showBigCircle) {
|
||||||
ctx.drawImage(
|
assets.images.projects.bottomScreen.circleBig.draw(
|
||||||
assets.projects.bottomScreen.circleBig,
|
ctx,
|
||||||
currentAnimation.position[0] - 44,
|
currentAnimation.position[0] - 44,
|
||||||
currentAnimation.position[1] - 44,
|
currentAnimation.position[1] - 44,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ onRender((ctx) => {
|
|||||||
ctx.strokeRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
ctx.strokeRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||||
|
|
||||||
// text
|
// text
|
||||||
ctx.drawImage(assets.projects.bottomScreen.popupTextBackground, 2, 146);
|
assets.images.projects.bottomScreen.popupTextBackground.draw(ctx, 2, 146);
|
||||||
ctx.font = "16px Pokemon DP Pro";
|
ctx.font = "16px Pokemon DP Pro";
|
||||||
ctx.textBaseline = "top";
|
ctx.textBaseline = "top";
|
||||||
const displayedText = text.value.slice(
|
const displayedText = text.value.slice(
|
||||||
@@ -135,10 +135,14 @@ onRender((ctx) => {
|
|||||||
|
|
||||||
// choice box
|
// choice box
|
||||||
if (textProgress.value >= 1) {
|
if (textProgress.value >= 1) {
|
||||||
ctx.drawImage(assets.projects.bottomScreen.popupChoiceBackground, 194, 98);
|
assets.images.projects.bottomScreen.popupChoiceBackground.draw(
|
||||||
|
ctx,
|
||||||
|
194,
|
||||||
|
98,
|
||||||
|
);
|
||||||
|
|
||||||
const y = selectedOption === "yes" ? YES_Y : NO_Y;
|
const y = selectedOption === "yes" ? YES_Y : NO_Y;
|
||||||
ctx.drawImage(assets.projects.bottomScreen.popupSelector, 200, y);
|
assets.images.projects.bottomScreen.popupSelector.draw(ctx, 200, y);
|
||||||
|
|
||||||
drawTextWithShadow(
|
drawTextWithShadow(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { onRender } = useScreen();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.projects.topScreen.background, 0, 0);
|
assets.images.projects.topScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const drawTextWithShadow2Lines = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.projects.topScreen.background, 0, 0);
|
assets.images.projects.topScreen.background.draw(ctx, 0, 0);
|
||||||
|
|
||||||
ctx.textBaseline = "hanging";
|
ctx.textBaseline = "hanging";
|
||||||
ctx.font = "16px Pokemon DP Pro";
|
ctx.font = "16px Pokemon DP Pro";
|
||||||
@@ -80,19 +80,19 @@ onRender((ctx) => {
|
|||||||
const offsetFromCurrent = i - store.currentProject;
|
const offsetFromCurrent = i - store.currentProject;
|
||||||
// TODO: project.id should be typed from useAssets, shouldn't be just a string
|
// TODO: project.id should be typed from useAssets, shouldn't be just a string
|
||||||
const thumbnailImage =
|
const thumbnailImage =
|
||||||
assets.projects.pokemons[
|
assets.images.projects.pokemons[
|
||||||
store.projects[i]!.id as keyof typeof assets.projects.pokemons
|
store.projects[i]!.id as keyof typeof assets.images.projects.pokemons
|
||||||
]!;
|
]!;
|
||||||
|
|
||||||
ctx.drawImage(
|
thumbnailImage.draw(
|
||||||
thumbnailImage,
|
ctx,
|
||||||
Math.floor(
|
Math.floor(
|
||||||
thumbnailCenterX +
|
thumbnailCenterX +
|
||||||
offsetFromCurrent * thumbnailSpacing -
|
offsetFromCurrent * thumbnailSpacing -
|
||||||
thumbnailImage.width / 2 +
|
thumbnailImage.rect.width / 2 +
|
||||||
store.offsetX,
|
store.offsetX,
|
||||||
),
|
),
|
||||||
Math.floor(thumbnailCenterY - thumbnailImage.height / 2),
|
Math.floor(thumbnailCenterY - thumbnailImage.rect.height / 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { onRender } = useScreen();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
|
assets.images.home.bottomScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -33,27 +33,27 @@ onRender((ctx) => {
|
|||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|
||||||
if (isOpen.value || animation.playing) {
|
if (isOpen.value || animation.playing) {
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.clock.time.draw(
|
||||||
assets.settings.topScreen.clock.time,
|
ctx,
|
||||||
48 - animation.stage2Offset,
|
48 - animation.stage2Offset,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.clock.date.draw(
|
||||||
assets.settings.topScreen.clock.date,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-96 + animation.stage2Offset + animation.stage1Offset,
|
-96 + animation.stage2Offset + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.clock.alarm.draw(
|
||||||
assets.settings.topScreen.clock.alarm,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.drawImage(assets.settings.topScreen.clock.clockActive, 0, 0);
|
assets.images.settings.topScreen.clock.clockActive.draw(ctx, 0, 0);
|
||||||
} else if (isAnyOtherMenuOpen.value) {
|
} else if (isAnyOtherMenuOpen.value) {
|
||||||
ctx.drawImage(assets.settings.topScreen.clock.clockDisabled, 0, 0);
|
assets.images.settings.topScreen.clock.clockDisabled.draw(ctx, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(assets.settings.topScreen.clock.clock, 0, 0);
|
assets.images.settings.topScreen.clock.clock.draw(ctx, 0, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,27 +33,27 @@ onRender((ctx) => {
|
|||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|
||||||
if (isOpen.value || animation.playing) {
|
if (isOpen.value || animation.playing) {
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.options.language.draw(
|
||||||
assets.settings.topScreen.options.language,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.options.gbaMode.draw(
|
||||||
assets.settings.topScreen.options.gbaMode,
|
ctx,
|
||||||
48 - animation.stage2Offset,
|
48 - animation.stage2Offset,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.options.startUp.draw(
|
||||||
assets.settings.topScreen.options.startUp,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-96 + animation.stage2Offset + animation.stage1Offset,
|
-96 + animation.stage2Offset + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.drawImage(assets.settings.topScreen.options.optionsActive, 0, 0);
|
assets.images.settings.topScreen.options.optionsActive.draw(ctx, 0, 0);
|
||||||
} else if (isAnyOtherMenuOpen.value) {
|
} else if (isAnyOtherMenuOpen.value) {
|
||||||
ctx.drawImage(assets.settings.topScreen.options.optionsDisabled, 0, 0);
|
assets.images.settings.topScreen.options.optionsDisabled.draw(ctx, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(assets.settings.topScreen.options.options, 0, 0);
|
assets.images.settings.topScreen.options.options.draw(ctx, 0, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ const isAnyOtherMenuOpen = computed(() => {
|
|||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
if (isAnyOtherMenuOpen.value) {
|
if (isAnyOtherMenuOpen.value) {
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.touchScreen.touchScreenDisabled.draw(
|
||||||
assets.settings.topScreen.touchScreen.touchScreenDisabled,
|
ctx,
|
||||||
props.x,
|
props.x,
|
||||||
props.y,
|
props.y,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.touchScreen.touchScreen.draw(
|
||||||
assets.settings.topScreen.touchScreen.touchScreen,
|
ctx,
|
||||||
props.x,
|
props.x,
|
||||||
props.y,
|
props.y,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ onClick((x, y) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onRender((ctx, deltaTime) => {
|
onRender((ctx, deltaTime) => {
|
||||||
ctx.drawImage(assets.settings.bottomScreen.user.colorPalette, 16, 32);
|
assets.images.settings.bottomScreen.user.colorPalette.draw(ctx, 16, 32);
|
||||||
|
|
||||||
// animate
|
// animate
|
||||||
const finalSelectorX = GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4;
|
const finalSelectorX = GRID_START_X + selectedCol * (CELL_SIZE + SPACING) - 4;
|
||||||
|
|||||||
@@ -33,32 +33,32 @@ onRender((ctx) => {
|
|||||||
ctx.translate(props.x, props.y);
|
ctx.translate(props.x, props.y);
|
||||||
|
|
||||||
if (isOpen.value || animation.playing) {
|
if (isOpen.value || animation.playing) {
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.user.birthday.draw(
|
||||||
assets.settings.topScreen.user.birthday,
|
ctx,
|
||||||
-48 + animation.stage2Offset,
|
-48 + animation.stage2Offset,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.user.userName.draw(
|
||||||
assets.settings.topScreen.user.userName,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.user.message.draw(
|
||||||
assets.settings.topScreen.user.message,
|
ctx,
|
||||||
48 - animation.stage2Offset,
|
48 - animation.stage2Offset,
|
||||||
-48 + animation.stage1Offset,
|
-48 + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
ctx.drawImage(
|
assets.images.settings.topScreen.user.color.draw(
|
||||||
assets.settings.topScreen.user.color,
|
ctx,
|
||||||
0,
|
0,
|
||||||
-96 + animation.stage2Offset + animation.stage1Offset,
|
-96 + animation.stage2Offset + animation.stage1Offset,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.drawImage(assets.settings.topScreen.user.userActive, 0, 0);
|
assets.images.settings.topScreen.user.userActive.draw(ctx, 0, 0);
|
||||||
} else if (isAnyOtherMenuOpen.value) {
|
} else if (isAnyOtherMenuOpen.value) {
|
||||||
ctx.drawImage(assets.settings.topScreen.user.userDisabled, 0, 0);
|
assets.images.settings.topScreen.user.userDisabled.draw(ctx, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
ctx.drawImage(assets.settings.topScreen.user.user, 0, 0);
|
assets.images.settings.topScreen.user.user.draw(ctx, 0, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,33 +49,34 @@ const { onRender, onClick } = useScreen();
|
|||||||
|
|
||||||
const Y = 30;
|
const Y = 30;
|
||||||
const SQUARE_HEIGHT = 49;
|
const SQUARE_HEIGHT = 49;
|
||||||
const ARROW_IMAGE_HEIGHT = assets.settings.bottomScreen.numberInputUp.height;
|
const ARROW_IMAGE_HEIGHT =
|
||||||
|
assets.images.settings.bottomScreen.numberInputUp.rect.height;
|
||||||
|
|
||||||
const upImage = computed(() => {
|
const upImage = computed(() => {
|
||||||
if (props.digits === 2) {
|
if (props.digits === 2) {
|
||||||
return props.disabled
|
return props.disabled
|
||||||
? assets.settings.bottomScreen.numberInputUpDisabled
|
? assets.images.settings.bottomScreen.numberInputUpDisabled
|
||||||
: assets.settings.bottomScreen.numberInputUp;
|
: assets.images.settings.bottomScreen.numberInputUp;
|
||||||
} else {
|
} else {
|
||||||
return props.disabled
|
return props.disabled
|
||||||
? assets.settings.bottomScreen.numberInputUpXlDisabled
|
? assets.images.settings.bottomScreen.numberInputUpXlDisabled
|
||||||
: assets.settings.bottomScreen.numberInputUpXl;
|
: assets.images.settings.bottomScreen.numberInputUpXl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const downImage = computed(() => {
|
const downImage = computed(() => {
|
||||||
if (props.digits === 2) {
|
if (props.digits === 2) {
|
||||||
return props.disabled
|
return props.disabled
|
||||||
? assets.settings.bottomScreen.numberInputDownDisabled
|
? assets.images.settings.bottomScreen.numberInputDownDisabled
|
||||||
: assets.settings.bottomScreen.numberInputDown;
|
: assets.images.settings.bottomScreen.numberInputDown;
|
||||||
} else {
|
} else {
|
||||||
return props.disabled
|
return props.disabled
|
||||||
? assets.settings.bottomScreen.numberInputDownXlDisabled
|
? assets.images.settings.bottomScreen.numberInputDownXlDisabled
|
||||||
: assets.settings.bottomScreen.numberInputDownXl;
|
: assets.images.settings.bottomScreen.numberInputDownXl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const squareWidth = computed(() => upImage.value.width);
|
const squareWidth = computed(() => upImage.value.rect.width);
|
||||||
|
|
||||||
const increase = () => {
|
const increase = () => {
|
||||||
const newValue = value.value + 1;
|
const newValue = value.value + 1;
|
||||||
@@ -89,7 +90,7 @@ const decrease = () => {
|
|||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
// arrow up
|
// arrow up
|
||||||
ctx.drawImage(upImage.value, props.x, Y);
|
upImage.value.draw(ctx, props.x, Y);
|
||||||
|
|
||||||
// outline
|
// outline
|
||||||
ctx.fillStyle = "#515151";
|
ctx.fillStyle = "#515151";
|
||||||
@@ -123,11 +124,7 @@ onRender((ctx) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// arrow down
|
// arrow down
|
||||||
ctx.drawImage(
|
downImage.value.draw(ctx, props.x, Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT);
|
||||||
downImage.value,
|
|
||||||
props.x,
|
|
||||||
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
// title
|
// title
|
||||||
ctx.font = "10px NDS10";
|
ctx.font = "10px NDS10";
|
||||||
@@ -140,7 +137,7 @@ onRender((ctx) => {
|
|||||||
// TODO: -10 is needed because fillTextCentered isn't using top baseline
|
// TODO: -10 is needed because fillTextCentered isn't using top baseline
|
||||||
// i will change that in the future (maybe)
|
// i will change that in the future (maybe)
|
||||||
Y + ARROW_IMAGE_HEIGHT * 2 + SQUARE_HEIGHT - 6,
|
Y + ARROW_IMAGE_HEIGHT * 2 + SQUARE_HEIGHT - 6,
|
||||||
downImage.value.width,
|
downImage.value.rect.width,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,7 +156,10 @@ useKeyDown((key) => {
|
|||||||
onClick((x, y) => {
|
onClick((x, y) => {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
if (
|
if (
|
||||||
rectContains([props.x, Y, upImage.value.width, ARROW_IMAGE_HEIGHT], [x, y])
|
rectContains(
|
||||||
|
[props.x, Y, upImage.value.rect.width, ARROW_IMAGE_HEIGHT],
|
||||||
|
[x, y],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
increase();
|
increase();
|
||||||
emit("select");
|
emit("select");
|
||||||
@@ -175,7 +175,7 @@ onClick((x, y) => {
|
|||||||
[
|
[
|
||||||
props.x,
|
props.x,
|
||||||
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT + 2,
|
Y + ARROW_IMAGE_HEIGHT + SQUARE_HEIGHT + 2,
|
||||||
downImage.value.width,
|
downImage.value.rect.width,
|
||||||
ARROW_IMAGE_HEIGHT,
|
ARROW_IMAGE_HEIGHT,
|
||||||
],
|
],
|
||||||
[x, y],
|
[x, y],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { onRender } = useScreen();
|
|||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.drawImage(assets.home.topScreen.background, 0, 0);
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
@@ -14,7 +13,6 @@ onRender((ctx) => {
|
|||||||
const CALENDAR_ROWS = 5;
|
const CALENDAR_ROWS = 5;
|
||||||
const CALENDAR_LEFT = 128;
|
const CALENDAR_LEFT = 128;
|
||||||
const CALENDAR_TOP = 64;
|
const CALENDAR_TOP = 64;
|
||||||
const SELECTOR_SIZE = 13;
|
|
||||||
|
|
||||||
ctx.fillStyle = "#343434";
|
ctx.fillStyle = "#343434";
|
||||||
|
|
||||||
@@ -25,16 +23,16 @@ onRender((ctx) => {
|
|||||||
const firstDay = new Date(year, month, 1).getDay();
|
const firstDay = new Date(year, month, 1).getDay();
|
||||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.calendar.draw(
|
||||||
assets.home.topScreen.calendar.calendar,
|
ctx,
|
||||||
CALENDAR_LEFT - 3,
|
CALENDAR_LEFT - 3,
|
||||||
CALENDAR_TOP - 33,
|
CALENDAR_TOP - 33,
|
||||||
);
|
);
|
||||||
|
|
||||||
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
const extraRow = CALENDAR_COLS * CALENDAR_ROWS - daysInMonth - firstDay < 0;
|
||||||
if (extraRow) {
|
if (extraRow) {
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.lastRow.draw(
|
||||||
assets.home.topScreen.calendar.lastRow,
|
ctx,
|
||||||
CALENDAR_LEFT - 3,
|
CALENDAR_LEFT - 3,
|
||||||
CALENDAR_TOP + 79,
|
CALENDAR_TOP + 79,
|
||||||
);
|
);
|
||||||
@@ -53,16 +51,11 @@ onRender((ctx) => {
|
|||||||
const cellTop = CALENDAR_TOP + col * 16;
|
const cellTop = CALENDAR_TOP + col * 16;
|
||||||
|
|
||||||
if (now.getDate() === day) {
|
if (now.getDate() === day) {
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.calendar.daySelectorsSheet.draw(
|
||||||
assets.home.topScreen.calendar.daySelectorsSheet,
|
ctx,
|
||||||
0,
|
|
||||||
(app.color.row * 4 + app.color.col) * SELECTOR_SIZE,
|
|
||||||
SELECTOR_SIZE,
|
|
||||||
SELECTOR_SIZE,
|
|
||||||
cellLeft + 1,
|
cellLeft + 1,
|
||||||
cellTop + 1,
|
cellTop + 1,
|
||||||
SELECTOR_SIZE,
|
{ colored: true },
|
||||||
SELECTOR_SIZE,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function drawLine(
|
|||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
ctx.translate(0, -16);
|
ctx.translate(0, -16);
|
||||||
|
|
||||||
ctx.drawImage(assets.home.topScreen.clock, 13, 45);
|
assets.images.home.topScreen.clock.draw(ctx, 13, 45);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ const { assets } = useAssets();
|
|||||||
|
|
||||||
const renderNotification = (
|
const renderNotification = (
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
image: HTMLImageElement,
|
image: AtlasImage,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
) => {
|
) => {
|
||||||
ctx.drawImage(assets.settings.topScreen.notification, 0, 0);
|
assets.images.settings.topScreen.notification.draw(ctx, 0, 0);
|
||||||
ctx.drawImage(image, 2, 2);
|
image.draw(ctx, 2, 2);
|
||||||
|
|
||||||
ctx.font = "10px NDS10";
|
ctx.font = "10px NDS10";
|
||||||
ctx.fillStyle = "#282828";
|
ctx.fillStyle = "#282828";
|
||||||
@@ -26,29 +26,29 @@ const renderNotification = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mainNotification = computed(() => ({
|
const mainNotification = computed(() => ({
|
||||||
image: assets.settings.topScreen.settings,
|
image: assets.images.settings.topScreen.settings,
|
||||||
title: $t("settings.title"),
|
title: $t("settings.title"),
|
||||||
description: $t("settings.description"),
|
description: $t("settings.description"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const IMAGES_MAP: Record<string, HTMLImageElement> = {
|
const IMAGES_MAP: Record<string, AtlasImage> = {
|
||||||
options: assets.settings.topScreen.options.options,
|
options: assets.images.settings.topScreen.options.options,
|
||||||
optionsStartUp: assets.settings.topScreen.options.startUp,
|
optionsStartUp: assets.images.settings.topScreen.options.startUp,
|
||||||
optionsLanguage: assets.settings.topScreen.options.language,
|
optionsLanguage: assets.images.settings.topScreen.options.language,
|
||||||
optionsGbaMode: assets.settings.topScreen.options.gbaMode,
|
optionsGbaMode: assets.images.settings.topScreen.options.gbaMode,
|
||||||
|
|
||||||
clock: assets.settings.topScreen.clock.clock,
|
clock: assets.images.settings.topScreen.clock.clock,
|
||||||
clockTime: assets.settings.topScreen.clock.time,
|
clockTime: assets.images.settings.topScreen.clock.time,
|
||||||
clockDate: assets.settings.topScreen.clock.date,
|
clockDate: assets.images.settings.topScreen.clock.date,
|
||||||
clockAlarm: assets.settings.topScreen.clock.alarm,
|
clockAlarm: assets.images.settings.topScreen.clock.alarm,
|
||||||
|
|
||||||
user: assets.settings.topScreen.user.user,
|
user: assets.images.settings.topScreen.user.user,
|
||||||
userUserName: assets.settings.topScreen.user.userName,
|
userUserName: assets.images.settings.topScreen.user.userName,
|
||||||
userBirthday: assets.settings.topScreen.user.birthday,
|
userBirthday: assets.images.settings.topScreen.user.birthday,
|
||||||
userMessage: assets.settings.topScreen.user.message,
|
userMessage: assets.images.settings.topScreen.user.message,
|
||||||
userColor: assets.settings.topScreen.user.color,
|
userColor: assets.images.settings.topScreen.user.color,
|
||||||
|
|
||||||
touchScreen: assets.settings.topScreen.touchScreen.touchScreen,
|
touchScreen: assets.images.settings.topScreen.touchScreen.touchScreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuNotification = computed(() => {
|
const menuNotification = computed(() => {
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { onRender } = useScreen();
|
const { onRender } = useScreen();
|
||||||
|
|
||||||
const app = useAppStore();
|
|
||||||
const { assets } = useAssets();
|
const { assets } = useAssets();
|
||||||
|
|
||||||
const BAR_WIDTH = 256;
|
|
||||||
const BAR_HEIGHT = 16;
|
|
||||||
|
|
||||||
onRender((ctx) => {
|
onRender((ctx) => {
|
||||||
const TEXT_Y = 11;
|
const TEXT_Y = 11;
|
||||||
|
|
||||||
ctx.drawImage(
|
assets.images.home.topScreen.statusBar.statusBarsSheet.draw(ctx, 0, 0, {
|
||||||
assets.home.topScreen.statusBar.statusBarsSheet,
|
colored: true,
|
||||||
0,
|
});
|
||||||
(app.color.row * 4 + app.color.col) * BAR_HEIGHT,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
BAR_WIDTH,
|
|
||||||
BAR_HEIGHT,
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff";
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.font = "7px NDS7";
|
ctx.font = "7px NDS7";
|
||||||
@@ -50,9 +38,9 @@ onRender((ctx) => {
|
|||||||
fillNumberCell(now.getMonth() + 1, 12, -1);
|
fillNumberCell(now.getMonth() + 1, 12, -1);
|
||||||
|
|
||||||
// icons
|
// icons
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.gbaDisplay, 210, 2);
|
assets.images.home.topScreen.statusBar.gbaDisplay.draw(ctx, 210, 2);
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.startupMode, 226, 2);
|
assets.images.home.topScreen.statusBar.startupMode.draw(ctx, 226, 2);
|
||||||
ctx.drawImage(assets.home.topScreen.statusBar.battery, 242, 4);
|
assets.images.home.topScreen.statusBar.battery.draw(ctx, 242, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
|||||||
@@ -1,29 +1,44 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
|
|
||||||
const imageCache = new Map<string, HTMLImageElement>();
|
type Rect = [number, number, number, number];
|
||||||
|
|
||||||
|
let atlasImage: HTMLImageElement | null = null;
|
||||||
const modelCache = new Map<string, THREE.Group>();
|
const modelCache = new Map<string, THREE.Group>();
|
||||||
|
|
||||||
const loaded = ref(0);
|
const loaded = ref(0);
|
||||||
const total = ref({{TOTAL}});
|
const total = ref({{TOTAL}});
|
||||||
const isReady = computed(() => loaded.value === total.value);
|
const isReady = computed(() => loaded.value === total.value);
|
||||||
|
|
||||||
const createImage = (path: string) => {
|
if (import.meta.client) {
|
||||||
if (imageCache.has(path)) {
|
atlasImage = document.createElement('img');
|
||||||
return imageCache.get(path)!;
|
atlasImage.src = '/nds/atlas.webp';
|
||||||
}
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
if (atlasImage.complete) {
|
||||||
img.src = path;
|
|
||||||
imageCache.set(path, img);
|
|
||||||
|
|
||||||
if (img.complete) {
|
|
||||||
loaded.value += 1;
|
loaded.value += 1;
|
||||||
} else {
|
} else {
|
||||||
img.onload = () => { loaded.value += 1 };
|
atlasImage.onload = () => {
|
||||||
|
loaded.value += 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawAtlasImage = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
[sx, sy, sw, sh]: Rect,
|
||||||
|
[dx, dy, dw, dh]: Rect,
|
||||||
|
opts?: Partial<{ colored: boolean }>,
|
||||||
|
): void => {
|
||||||
|
if (!atlasImage) return;
|
||||||
|
|
||||||
|
if (opts?.colored) {
|
||||||
|
const app = useAppStore();
|
||||||
|
sh /= 16;
|
||||||
|
sy += (app.color.row * 4 + app.color.col) * sh;
|
||||||
|
dh = sh;
|
||||||
}
|
}
|
||||||
|
|
||||||
return img;
|
ctx.drawImage(atlasImage, sx, sy, sw, sh, dx, dy, dw, dh);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createModel = (path: string) => {
|
const createModel = (path: string) => {
|
||||||
@@ -54,7 +69,28 @@ const createModel = (path: string) => {
|
|||||||
return model;
|
return model;
|
||||||
};
|
};
|
||||||
|
|
||||||
const assets = {{ASSETS}};
|
export type AtlasImage = {
|
||||||
|
draw: (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
opts?: Partial<{ colored: boolean }>,
|
||||||
|
) => void;
|
||||||
|
rect: { x: number; y: number; width: number; height: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageTree = {
|
||||||
|
[key: string]: AtlasImage | ImageTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModelTree = {
|
||||||
|
[key: string]: THREE.Group | ModelTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assets = {
|
||||||
|
images: {{IMAGES}} as const satisfies ImageTree,
|
||||||
|
models: {{MODELS}} as const satisfies ModelTree,
|
||||||
|
};
|
||||||
|
|
||||||
export const useAssets = () => {
|
export const useAssets = () => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,14 +2,56 @@ import { defineNuxtModule, useLogger } from "@nuxt/kit";
|
|||||||
import { readdir, readFile, writeFile } from "fs/promises";
|
import { readdir, readFile, writeFile } from "fs/promises";
|
||||||
import { join, relative, parse } from "path";
|
import { join, relative, parse } from "path";
|
||||||
import { existsSync, watch } from "fs";
|
import { existsSync, watch } from "fs";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
type AssetsTree = {
|
type AtlasRect = {
|
||||||
[key: string]: string | AssetsTree;
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageTree = {
|
||||||
|
[key: string]: AtlasRect | ImageTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModelTree = {
|
||||||
|
[key: string]: string | ModelTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageData = {
|
||||||
|
path: string;
|
||||||
|
buffer: Buffer;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = [".png", ".webp"];
|
const IMAGE_EXTENSIONS = [".png", ".webp"];
|
||||||
const MODELS_EXTENSIONS = [".gltf"];
|
const MODEL_EXTENSIONS = [".gltf"];
|
||||||
const ASSETS_EXTENSIONS = [...IMAGE_EXTENSIONS, ...MODELS_EXTENSIONS];
|
const MAX_WIDTH = 1024;
|
||||||
|
|
||||||
|
const toCamelCase = (str: string) =>
|
||||||
|
str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replaceAll(".", "_");
|
||||||
|
|
||||||
|
const scanByExt = async (
|
||||||
|
dir: string,
|
||||||
|
extensions: string[],
|
||||||
|
): Promise<string[]> => {
|
||||||
|
if (!existsSync(dir)) return [];
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
results.push(...(await scanByExt(fullPath, extensions)));
|
||||||
|
} else if (extensions.includes(parse(fullPath).ext.toLowerCase())) {
|
||||||
|
results.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
export default defineNuxtModule({
|
export default defineNuxtModule({
|
||||||
meta: {
|
meta: {
|
||||||
@@ -19,125 +61,237 @@ export default defineNuxtModule({
|
|||||||
defaults: {},
|
defaults: {},
|
||||||
async setup(_, nuxt) {
|
async setup(_, nuxt) {
|
||||||
const logger = useLogger("asset-generator");
|
const logger = useLogger("asset-generator");
|
||||||
const publicDir = join(nuxt.options.rootDir, "public/nds");
|
const rootDir = nuxt.options.rootDir;
|
||||||
const templateFile = join(
|
const imagesDir = join(rootDir, "public/nds/images");
|
||||||
nuxt.options.rootDir,
|
const modelsDir = join(rootDir, "public/nds/models");
|
||||||
"app/composables/useAssets.ts.in",
|
const templateFile = join(rootDir, "app/composables/useAssets.ts.in");
|
||||||
);
|
const outputFile = join(rootDir, "app/composables/useAssets.ts");
|
||||||
const outputFile = join(
|
const atlasOutputPath = join(rootDir, "public/nds/atlas.webp");
|
||||||
nuxt.options.rootDir,
|
|
||||||
"app/composables/useAssets.ts",
|
const loadImages = async (
|
||||||
|
paths: string[],
|
||||||
|
): Promise<Map<string, ImageData>> => {
|
||||||
|
const images = new Map<string, ImageData>();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
paths.map(async (path) => {
|
||||||
|
try {
|
||||||
|
const [metadata, buffer] = await Promise.all([
|
||||||
|
sharp(path).metadata(),
|
||||||
|
sharp(path).png().toBuffer(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (metadata.width && metadata.height) {
|
||||||
|
images.set(path, {
|
||||||
|
path,
|
||||||
|
buffer,
|
||||||
|
width: metadata.width,
|
||||||
|
height: metadata.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.warn(`Failed to load ${path}, skipping...`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasExt = (filename: string, exts: string[]): boolean => {
|
return images;
|
||||||
const ext = parse(filename).ext.toLowerCase();
|
|
||||||
return exts.includes(ext);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const scanDirectory = async (dir: string): Promise<string[]> => {
|
const packImages = (images: ImageData[]) => {
|
||||||
const assets: string[] = [];
|
const sorted = images.toSorted((a, b) => b.height - a.height);
|
||||||
const entries = await readdir(dir, { withFileTypes: true });
|
const rects = new Map<string, AtlasRect>();
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
let rowHeight = 0;
|
||||||
|
let maxWidth = 0;
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const img of sorted) {
|
||||||
const fullPath = join(dir, entry.name);
|
if (x + img.width > MAX_WIDTH) {
|
||||||
if (entry.isDirectory()) {
|
x = 0;
|
||||||
const assetFiles = await scanDirectory(fullPath);
|
y += rowHeight;
|
||||||
assets.push(...assetFiles);
|
rowHeight = 0;
|
||||||
} else if (hasExt(fullPath, ASSETS_EXTENSIONS)) {
|
|
||||||
assets.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return assets;
|
rects.set(img.path, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
x += img.width;
|
||||||
|
rowHeight = Math.max(rowHeight, img.height);
|
||||||
|
maxWidth = Math.max(maxWidth, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rects, width: maxWidth, height: y + rowHeight };
|
||||||
};
|
};
|
||||||
|
|
||||||
const toCamelCase = (str: string): string => {
|
const buildImageTree = (
|
||||||
return str
|
imagePaths: string[],
|
||||||
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
rects: Map<string, AtlasRect>,
|
||||||
.replaceAll(".", "_");
|
): ImageTree => {
|
||||||
};
|
const tree: ImageTree = {};
|
||||||
|
|
||||||
const buildAssetsTree = (assets: string[], baseDir: string): AssetsTree => {
|
for (const path of imagePaths) {
|
||||||
const tree: AssetsTree = {};
|
const parts = relative(imagesDir, path).split("/");
|
||||||
|
const fileName = parse(parts.at(-1)!).name;
|
||||||
|
|
||||||
for (const assetPath of assets) {
|
let node = tree;
|
||||||
const relativePath = relative(baseDir, assetPath);
|
for (const part of parts.slice(0, -1)) {
|
||||||
const parts = relativePath.split("/");
|
const key = toCamelCase(part);
|
||||||
const filename = parse(parts[parts.length - 1]!).name;
|
node[key] ??= {};
|
||||||
|
node = node[key] as ImageTree;
|
||||||
let current = tree;
|
|
||||||
// start at 1 to skip images/, models/
|
|
||||||
for (let i = 1; i < parts.length - 1; i += 1) {
|
|
||||||
const key = toCamelCase(parts[i]!);
|
|
||||||
current[key] ??= {};
|
|
||||||
current = current[key] as AssetsTree;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current[toCamelCase(filename)] = `/${relativePath}`;
|
const rect = rects.get(path);
|
||||||
|
if (rect) node[toCamelCase(fileName)] = rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree;
|
return tree;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatorFn = (filePath: string): string => {
|
const buildModelTree = (modelPaths: string[]): ModelTree => {
|
||||||
if (hasExt(filePath, IMAGE_EXTENSIONS)) return "createImage";
|
const tree: ModelTree = {};
|
||||||
if (hasExt(filePath, MODELS_EXTENSIONS)) return "createModel";
|
|
||||||
throw new Error(`No matching generator for '${filePath}'`);
|
for (const path of modelPaths) {
|
||||||
|
const parts = relative(modelsDir, path).split("/");
|
||||||
|
const fileName = parse(parts.at(-1)!).name;
|
||||||
|
|
||||||
|
let node = tree;
|
||||||
|
for (const part of parts.slice(0, -1)) {
|
||||||
|
const key = toCamelCase(part);
|
||||||
|
node[key] ??= {};
|
||||||
|
node = node[key] as ModelTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
node[toCamelCase(fileName)] =
|
||||||
|
`/nds/models/${relative(modelsDir, path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateAssetsObject = (tree: AssetsTree, indent = 0): string => {
|
const generateImageCode = (tree: ImageTree, indent = 0): string => {
|
||||||
const spaces = " ".repeat(indent);
|
|
||||||
const entries = Object.entries(tree);
|
const entries = Object.entries(tree);
|
||||||
if (!entries.length) return "{}";
|
if (!entries.length) return "{}";
|
||||||
|
|
||||||
const lines = entries.map(([key, value]) =>
|
const sp = " ".repeat(indent);
|
||||||
typeof value === "string"
|
const lines = entries.map(([key, value]) => {
|
||||||
? `${spaces} ${key}: ${generatorFn(value)}("/nds${value}"),`
|
if (value && typeof value === "object" && "x" in value) {
|
||||||
: `${spaces} ${key}: ${generateAssetsObject(value, indent + 1)},`,
|
const { x, y, width, height } = value;
|
||||||
);
|
return `${sp} ${key}: {
|
||||||
|
${sp} draw: (ctx, x, y, opts?) => drawAtlasImage(ctx, [${x}, ${y}, ${width}, ${height}], [x, y, ${width}, ${height}], opts),
|
||||||
|
${sp} rect: { x: ${x}, y: ${y}, width: ${width}, height: ${height} },
|
||||||
|
${sp} }`;
|
||||||
|
}
|
||||||
|
return `${sp} ${key}: ${generateImageCode(value, indent + 1)}`;
|
||||||
|
});
|
||||||
|
|
||||||
return `{\n${lines.join("\n")}\n${spaces}}`;
|
return `{\n${lines.join(",\n")}\n${sp}}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateAssetsFile = async () => {
|
const generateModelCode = (tree: ModelTree, indent = 0): string => {
|
||||||
|
const entries = Object.entries(tree);
|
||||||
|
if (!entries.length) return "{}";
|
||||||
|
|
||||||
|
const sp = " ".repeat(indent);
|
||||||
|
const lines = entries.map(([key, value]) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return `${sp} ${key}: createModel("${value}")`;
|
||||||
|
}
|
||||||
|
return `${sp} ${key}: ${generateModelCode(value, indent + 1)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `{\n${lines.join(",\n")},\n${sp}}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateAssets = async () => {
|
||||||
try {
|
try {
|
||||||
if (!existsSync(publicDir)) {
|
const imagePaths = await scanByExt(imagesDir, IMAGE_EXTENSIONS);
|
||||||
logger.warn("No public directory found");
|
const modelPaths = await scanByExt(modelsDir, MODEL_EXTENSIONS);
|
||||||
return;
|
|
||||||
|
const totalAssets = (imagePaths.length > 0 ? 1 : 0) + modelPaths.length;
|
||||||
|
|
||||||
|
let imageCode = "{}";
|
||||||
|
let atlasWidth = 0;
|
||||||
|
let atlasHeight = 0;
|
||||||
|
|
||||||
|
if (imagePaths.length > 0) {
|
||||||
|
const imageMap = await loadImages(imagePaths);
|
||||||
|
const images = Array.from(imageMap.values());
|
||||||
|
|
||||||
|
const { rects, width, height } = packImages(images);
|
||||||
|
atlasWidth = width;
|
||||||
|
atlasHeight = height;
|
||||||
|
|
||||||
|
const atlasBuffer = await sharp({
|
||||||
|
create: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite(
|
||||||
|
images.map((img) => {
|
||||||
|
const rect = rects.get(img.path)!;
|
||||||
|
return { input: img.buffer, left: rect.x, top: rect.y };
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.webp({ lossless: true })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
await writeFile(atlasOutputPath, atlasBuffer);
|
||||||
|
|
||||||
|
const imageTree = buildImageTree(imagePaths, rects);
|
||||||
|
imageCode = generateImageCode(imageTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets = await scanDirectory(publicDir);
|
const modelTree = buildModelTree(modelPaths);
|
||||||
const assetsTree = buildAssetsTree(assets, publicDir);
|
const modelCode = generateModelCode(modelTree);
|
||||||
const assetsObject = generateAssetsObject(assetsTree);
|
|
||||||
|
|
||||||
const template = await readFile(templateFile, "utf-8");
|
const template = await readFile(templateFile, "utf-8");
|
||||||
const fileContent = template
|
const code = template
|
||||||
.replace("{{TOTAL}}", assets.length.toString())
|
.replace("{{TOTAL}}", totalAssets.toString())
|
||||||
.replace("{{ASSETS}}", assetsObject);
|
.replace("{{IMAGES}}", imageCode)
|
||||||
|
.replace("{{MODELS}}", modelCode);
|
||||||
|
|
||||||
await writeFile(outputFile, fileContent, "utf-8");
|
await writeFile(outputFile, code, "utf-8");
|
||||||
logger.success(`Generated useAssets.ts with ${assets.length} assets`);
|
logger.success(
|
||||||
|
`Generated assets with ${imagePaths.length} images (${atlasWidth}x${atlasHeight}) and ${modelPaths.length} models`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error generating assets file:", error);
|
logger.error("Error generating assets:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
nuxt.hook("build:before", async () => {
|
nuxt.hook("build:before", generateAssets);
|
||||||
await generateAssetsFile();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nuxt.options.dev) {
|
if (nuxt.options.dev) {
|
||||||
nuxt.hook("ready", () => {
|
const watchAndRegenerate = (
|
||||||
watch(publicDir, { recursive: true }, async (_, filePath) => {
|
dir: string,
|
||||||
if (filePath && hasExt(filePath, ASSETS_EXTENSIONS)) {
|
extensions: string[],
|
||||||
logger.info(`Detected change: ${filePath}`);
|
type: string,
|
||||||
await generateAssetsFile();
|
) => {
|
||||||
|
watch(dir, { recursive: true }, (_, filePath) => {
|
||||||
|
if (
|
||||||
|
filePath &&
|
||||||
|
extensions.includes(parse(filePath).ext.toLowerCase())
|
||||||
|
) {
|
||||||
|
logger.info(`Detected ${type} change: ${filePath}`);
|
||||||
|
generateAssets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
watch(templateFile, async () => {
|
nuxt.hook("ready", () => {
|
||||||
|
watchAndRegenerate(imagesDir, IMAGE_EXTENSIONS, "image");
|
||||||
|
watchAndRegenerate(modelsDir, MODEL_EXTENSIONS, "model");
|
||||||
|
watch(templateFile, () => {
|
||||||
logger.info("Template file changed");
|
logger.info("Template file changed");
|
||||||
await generateAssetsFile();
|
generateAssets();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
"nuxt": "4.2.1",
|
"nuxt": "4.2.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"typescript": "5.9.3"
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
|||||||
prettier:
|
prettier:
|
||||||
specifier: 3.6.2
|
specifier: 3.6.2
|
||||||
version: 3.6.2
|
version: 3.6.2
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.34.5
|
||||||
|
version: 0.34.5
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.9.3
|
specifier: 5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -11047,8 +11050,7 @@ snapshots:
|
|||||||
"@iconify/types": 2.0.0
|
"@iconify/types": 2.0.0
|
||||||
vue: 3.5.25(typescript@5.9.3)
|
vue: 3.5.25(typescript@5.9.3)
|
||||||
|
|
||||||
"@img/colour@1.0.0":
|
"@img/colour@1.0.0": {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64@0.34.5":
|
"@img/sharp-darwin-arm64@0.34.5":
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -17020,7 +17022,6 @@ snapshots:
|
|||||||
"@img/sharp-win32-arm64": 0.34.5
|
"@img/sharp-win32-arm64": 0.34.5
|
||||||
"@img/sharp-win32-ia32": 0.34.5
|
"@img/sharp-win32-ia32": 0.34.5
|
||||||
"@img/sharp-win32-x64": 0.34.5
|
"@img/sharp-win32-x64": 0.34.5
|
||||||
optional: true
|
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user