feat: centralize all screen related callbacks in useScreen

This commit is contained in:
2025-12-29 21:01:19 +01:00
parent 4960bef2fc
commit d77d595370
42 changed files with 184 additions and 128 deletions

View File

@@ -12,6 +12,8 @@ const props = withDefaults(
},
);
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
@@ -19,7 +21,7 @@ const BAR_WIDTH = 256;
const BAR_HEIGHT = 24;
const TITLE_Y = 5;
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
// top bar

View File

@@ -9,6 +9,8 @@ const props = withDefaults(
},
);
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
@@ -17,7 +19,7 @@ const CORNER_SIZE = 11;
let [currentX, currentY, currentWidth, currentHeight] = props.rect;
useRender((ctx) => {
onRender((ctx) => {
const [targetX, targetY, targetWidth, targetHeight] = props.rect;
const dx = targetX - currentX;
const dy = targetY - currentY;

View File

@@ -1,6 +1,4 @@
<script setup lang="ts">
const { assets } = useAssets();
const props = defineProps<{
yOffset: number;
opacity?: number;
@@ -13,6 +11,10 @@ const emit = defineEmits<{
activateB: [];
}>();
const { onRender, onClick } = useScreen();
const { assets } = useAssets();
const BUTTON_WIDTH = assets.common.button.width;
const BUTTON_HEIGHT = assets.common.button.height;
const LETTER_WIDTH = assets.common.B.width;
@@ -20,7 +22,7 @@ const LETTER_WIDTH = assets.common.B.width;
const B_BUTTON: Rect = [31, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
const A_BUTTON: Rect = [144, 172, BUTTON_WIDTH, BUTTON_HEIGHT];
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = props.opacity ?? 1;
ctx.font = "10px NDS10";
@@ -46,7 +48,7 @@ useRender((ctx) => {
drawButton(assets.common.A, props.aLabel, 144, 0);
});
useScreenClick((x, y) => {
onClick((x, y) => {
if (props.yOffset !== 0) return;
if (rectContains(B_BUTTON, [x, y])) {
emit("activateB");

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import Buttons from "./Buttons.vue";
const { onRender } = useScreen();
const { assets } = useAssets();
const { close, state } = useConfirmationModal();
@@ -23,7 +25,7 @@ const handleActivateB = () => {
close();
};
useRender((ctx) => {
onRender((ctx) => {
if (!state.value.isVisible) return;
ctx.beginPath();
@@ -39,7 +41,7 @@ useRender((ctx) => {
ctx.fillStyle = "#ffffff";
fillTextCentered(ctx, state.value.text, BG_X, TEXT_Y, BG_WIDTH);
});
}, 100);
onUnmounted(() => {
close();

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
const store = useContactStore();
const { onRender } = useScreen();
const store = useContactStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
ctx.globalAlpha = store.isIntro
? store.intro.stage2Opacity

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
const store = useContactStore();
const { onRender } = useScreen();
const store = useContactStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = store.isIntro
? store.intro.stage3Opacity
: store.outro.stage1Opacity;

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
const store = useContactStore();
const { onRender } = useScreen();
const store = useContactStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.home.topScreen.background, 0, 0);
ctx.globalAlpha = store.isIntro
? store.intro.stage2Opacity

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
const store = useContactStore();
const { onRender } = useScreen();
const store = useContactStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity
: store.outro.stage2Opacity;

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
const { onRender } = useScreen();
// text color:
const store = useContactStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = store.outro.stage2Opacity;
ctx.font = "10px NDS10";

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
const { onRender } = useScreen();
const store = useHomeStore();
const app = useAppStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
});

View File

@@ -6,7 +6,9 @@ const props = defineProps<{
image: HTMLImageElement;
}>();
useRender((ctx) => {
const { onRender } = useScreen();
onRender((ctx) => {
ctx.globalAlpha = props.opacity;
ctx.drawImage(props.image, props.x, props.y);
});

View File

@@ -2,6 +2,8 @@
import Button from "./Button.vue";
import Selector from "~/components/Common/ButtonSelector.vue";
const { onRender } = useScreen();
const store = useHomeStore();
const { assets } = useAssets();
@@ -66,7 +68,7 @@ const getOpacity = (button?: (typeof selectedButton)["value"]) => {
return 1;
};
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = getOpacity();
ctx.font = "10px NDS10";
ctx.fillStyle = "#a2a2a2";

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
const { onRender } = useScreen();
const store = useHomeStore();
const app = useAppStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = app.booted ? 1 : store.intro.stage1Opacity;
ctx.drawImage(assets.home.topScreen.background, 0, 0);
});

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
const { onRender } = useScreen();
// NOTE: calendar background is handled by TopScreenBackground
const app = useAppStore();
const store = useHomeStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.fillStyle = "black";
ctx.font = "7px NDS7";

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
const CENTER_X = 63;
const CENTER_Y = 95;
const { onRender } = useScreen();
const app = useAppStore();
const store = useHomeStore();
const { assets } = useAssets();
const CENTER_X = 63;
const CENTER_Y = 95;
function drawLine(
ctx: CanvasRenderingContext2D,
x0: number,
@@ -52,7 +53,7 @@ function drawLine(
}
}
useRender((ctx) => {
onRender((ctx) => {
ctx.globalAlpha = store.isIntro
? store.intro.stage1Opacity
: store.isOutro && store.outro.animateTop

View File

@@ -1,13 +1,14 @@
<script setup lang="ts">
const { onRender } = useScreen();
const app = useAppStore();
const store = useHomeStore();
const { assets } = useAssets();
const BAR_WIDTH = 256;
const BAR_HEIGHT = 16;
useRender((ctx) => {
onRender((ctx) => {
const TEXT_Y = 11;
ctx.translate(0, store.isIntro ? store.intro.statusBarY : 0);

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
const { onRender } = useScreen();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.projects.bottomScreen.background, 0, 0);
});
defineOptions({

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import gsap from "gsap";
const store = useProjectsStore();
const { onRender, onClick } = useScreen();
const store = useProjectsStore();
const { assets } = useAssets();
const CLICK_RADIUS = 22;
@@ -78,7 +79,7 @@ const startButtonAnimation = (type: ButtonType) => {
});
};
useScreenClick((x, y) => {
onClick((x, y) => {
if (
currentAnimation ||
store.isIntro ||
@@ -111,7 +112,7 @@ useScreenClick((x, y) => {
}
});
useRender((ctx) => {
onRender((ctx) => {
// Draw disabled buttons
if (store.currentProject === 0) {
ctx.drawImage(

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import gsap from "gsap";
const { onRender, onClick } = useScreen();
const { assets } = useAssets();
const store = useProjectsStore();
@@ -77,7 +79,7 @@ useKeyUp((key) => {
}
});
useScreenClick((x, y) => {
onClick((x, y) => {
if (
!store.showConfirmationPopup ||
textProgress.value < 1 ||
@@ -113,7 +115,7 @@ const drawTextWithShadow = (
ctx.fillText(text, x, y);
};
useRender((ctx) => {
onRender((ctx) => {
if (!store.showConfirmationPopup) return;
// frame

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
const { onRender } = useScreen();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.projects.topScreen.background, 0, 0);
});

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
const store = useProjectsStore();
const { onRender } = useScreen();
const store = useProjectsStore();
const { assets } = useAssets();
const drawTextWithShadow = (
@@ -54,7 +55,7 @@ const drawTextWithShadow2Lines = (
drawTextWithShadow(ctx, line2Color, secondLine, x, y + 16);
};
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.projects.topScreen.background, 0, 0);
ctx.textBaseline = "hanging";

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
const canvas = useTemplateRef("canvas");
const renderCallbacks = new Set<RenderCallback>();
const renderCallbacks = new Map<RenderCallback, number>();
const screenClickCallbacks = new Set<ScreenClickCallback>();
const screenMouseWheelCallbacks = new Set<ScreenMouseWheelCallback>();
@@ -10,8 +10,8 @@ let animationFrameId: number | null = null;
let lastFrameTime = 0;
let lastRealFrameTime = 0;
const registerRenderCallback = (callback: RenderCallback) => {
renderCallbacks.add(callback);
const registerRenderCallback = (callback: RenderCallback, zIndex = 0) => {
renderCallbacks.set(callback, zIndex);
return () => renderCallbacks.delete(callback);
};
@@ -59,7 +59,11 @@ const renderFrame = (timestamp: number) => {
// render
ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
for (const callback of renderCallbacks) {
const sortedCallbacks = Array.from(renderCallbacks.entries())
.sort((a, b) => a[1] - b[1])
.map(([callback]) => callback);
for (const callback of sortedCallbacks) {
ctx.save();
callback(ctx, deltaTime, lastRealFrameTime);

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
const { onRender } = useScreen();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.home.bottomScreen.background, 0, 0);
});

View File

@@ -4,6 +4,8 @@ const props = defineProps<{
y: number;
}>();
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
@@ -27,7 +29,7 @@ const isAnyOtherMenuOpen = computed(() => {
const animation = useMenuAnimation("clock", isOpen);
useRender((ctx) => {
onRender((ctx) => {
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
useRender((ctx) => {
const { onRender } = useScreen();
onRender((ctx) => {
ctx.font = "10px NDS10";
ctx.fillStyle = "#000000";
ctx.fillText("GBA Mode", 10, 20);

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
useRender((ctx) => {
const { onRender } = useScreen();
onRender((ctx) => {
ctx.font = "10px NDS10";
ctx.fillStyle = "#000000";
ctx.fillText("Language", 10, 20);

View File

@@ -4,6 +4,8 @@ const props = defineProps<{
y: number;
}>();
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
@@ -27,7 +29,7 @@ const isAnyOtherMenuOpen = computed(() => {
const animation = useMenuAnimation("options", isOpen);
useRender((ctx) => {
onRender((ctx) => {
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
useRender((ctx) => {
const { onRender } = useScreen();
onRender((ctx) => {
ctx.font = "10px NDS10";
ctx.fillStyle = "#000000";
ctx.fillText("Startup", 10, 20);

View File

@@ -4,6 +4,8 @@ const props = defineProps<{
y: number;
}>();
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
@@ -23,7 +25,7 @@ const isAnyOtherMenuOpen = computed(() => {
return false;
});
useRender((ctx) => {
onRender((ctx) => {
if (isAnyOtherMenuOpen.value) {
ctx.drawImage(
assets.settings.topScreen.touchScreen.touchScreenDisabled,

View File

@@ -1,4 +1,10 @@
<script setup lang="ts">
const { onRender, onClick } = useScreen();
const app = useAppStore();
const store = useSettingsStore();
const { assets } = useAssets();
const GRID_SIZE = 4;
const GRID_START_X = 32;
const GRID_START_Y = 40;
@@ -6,10 +12,6 @@ const CELL_SIZE = 16;
const SPACING = 16;
const ANIMATION_SPEED = 475;
const app = useAppStore();
const store = useSettingsStore();
const { assets } = useAssets();
const originalSelectedCol = app.color.col;
const originalSelectedRow = app.color.row;
@@ -41,7 +43,7 @@ useKeyDown((key) => {
}
});
useScreenClick((x, y) => {
onClick((x, y) => {
const relativeX = x - GRID_START_X;
const relativeY = y - GRID_START_Y;
@@ -59,7 +61,7 @@ useScreenClick((x, y) => {
}
});
useRender((ctx, deltaTime) => {
onRender((ctx, deltaTime) => {
ctx.drawImage(assets.settings.bottomScreen.user.colorPalette, 16, 32);
// animate
@@ -109,7 +111,6 @@ const handleConfirm = () => {
app.save();
openModal({
text: "hey",
showButtons: false,
});
setTimeout(() => {
closeModal();

View File

@@ -4,6 +4,8 @@ const props = defineProps<{
y: number;
}>();
const { onRender } = useScreen();
const settingsStore = useSettingsStore();
const menusContext = inject<{
isSubmenuSelected: ComputedRef<boolean>;
@@ -27,7 +29,7 @@ const isAnyOtherMenuOpen = computed(() => {
const animation = useMenuAnimation("user", isOpen);
useRender((ctx) => {
onRender((ctx) => {
ctx.translate(props.x, props.y);
if (isOpen.value || animation.playing) {

View File

@@ -1,7 +1,9 @@
<script setup lang="ts">
const { onRender } = useScreen();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.drawImage(assets.home.topScreen.background, 0, 0);
});

View File

@@ -1,8 +1,10 @@
<script setup lang="ts">
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
useRender((ctx) => {
onRender((ctx) => {
ctx.fillStyle = "black";
ctx.font = "7px NDS7";

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
const CENTER_X = 63;
const CENTER_Y = 95;
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
const CENTER_X = 63;
const CENTER_Y = 95;
function drawLine(
ctx: CanvasRenderingContext2D,
x0: number,
@@ -50,7 +52,7 @@ function drawLine(
}
}
useRender((ctx) => {
onRender((ctx) => {
ctx.translate(0, -16);
ctx.drawImage(assets.home.topScreen.clock, 13, 45);

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
const store = useSettingsStore();
const { onRender } = useScreen();
const store = useSettingsStore();
const { assets } = useAssets();
const renderNotification = (
@@ -76,7 +77,7 @@ const submenuNotification = computed(() => {
};
});
useRender((ctx) => {
onRender((ctx) => {
let count = 1;
if (menuNotification.value) count++;
if (submenuNotification.value) count++;

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
const { onRender } = useScreen();
const app = useAppStore();
const { assets } = useAssets();
const BAR_WIDTH = 256;
const BAR_HEIGHT = 16;
useRender((ctx) => {
onRender((ctx) => {
const TEXT_Y = 11;
ctx.drawImage(

View File

@@ -4,11 +4,13 @@ const props = withDefaults(defineProps<{ x?: number; y?: number }>(), {
y: 0,
});
const { onRender } = useScreen();
const SAMPLES = 60;
let average = { deltaTime: 0, realDeltaTime: 0 };
const lastFrames: (typeof average)[] = [];
useRender((ctx, deltaTime, realDeltaTime) => {
onRender((ctx, deltaTime, realDeltaTime) => {
lastFrames.push({ deltaTime, realDeltaTime });
if (lastFrames.length > SAMPLES) {

View File

@@ -29,7 +29,9 @@ export const useButtonNavigation = <T extends Record<string, ButtonConfig>>({
const nextButton = ref<keyof T | undefined>();
useScreenClick((x: number, y: number) => {
const { onClick } = useScreen();
onClick((x: number, y: number) => {
if (modalState.value.isOpen || disabled?.value) return;
for (const [buttonName, config] of Object.entries(buttons) as [

View File

@@ -1,22 +0,0 @@
export type RenderCallback = (
ctx: CanvasRenderingContext2D,
deltaTime: number,
realDeltaTime: number,
) => void;
export const useRender = (callback: RenderCallback) => {
const registerRenderCallback = inject<
(callback: RenderCallback) => () => void
>("registerRenderCallback");
onMounted(() => {
if (!registerRenderCallback) {
throw new Error(
"Missing registerRenderCallback - useRender must be used within a Screen component",
);
}
const unregister = registerRenderCallback(callback);
onUnmounted(unregister);
});
};

View File

@@ -0,0 +1,50 @@
export type RenderCallback = (
ctx: CanvasRenderingContext2D,
deltaTime: number,
realDeltaTime: number,
) => void;
export type ScreenClickCallback = (x: number, y: number) => void;
export type ScreenMouseWheelCallback = (deltaY: number, deltaX: number) => void;
export const useScreen = () => {
const registerRender = inject<
(cb: RenderCallback, zIndex?: number) => () => void
>("registerRenderCallback");
const registerClick = inject<(cb: ScreenClickCallback) => () => void>(
"registerScreenClickCallback",
);
const registerWheel = inject<(cb: ScreenMouseWheelCallback) => () => void>(
"registerScreenMouseWheelCallback",
);
const onRender = (callback: RenderCallback, zIndex = 0) => {
onMounted(() => {
if (!registerRender)
throw new Error("useScreen must be used within a Screen component");
const unregister = registerRender(callback, zIndex);
onUnmounted(unregister);
});
};
const onClick = (callback: ScreenClickCallback) => {
onMounted(() => {
if (!registerClick)
throw new Error("useScreen must be used within a Screen component");
const unregister = registerClick(callback);
onUnmounted(unregister);
});
};
const onMouseWheel = (callback: ScreenMouseWheelCallback) => {
onMounted(() => {
if (!registerWheel)
throw new Error("useScreen must be used within a Screen component");
const unregister = registerWheel(callback);
onUnmounted(unregister);
});
};
return { onRender, onClick, onMouseWheel };
};

View File

@@ -1,18 +0,0 @@
export type ScreenClickCallback = (x: number, y: number) => void;
export const useScreenClick = (callback: ScreenClickCallback) => {
const registerScreenClickCallback = inject<
(callback: ScreenClickCallback) => () => void
>("registerScreenClickCallback");
onMounted(() => {
if (!registerScreenClickCallback) {
throw new Error(
"Missing registerScreenClickCallback - useScreenClick must be used within a Screen component",
);
}
const unregister = registerScreenClickCallback(callback);
onUnmounted(unregister);
});
};

View File

@@ -1,18 +0,0 @@
export type ScreenMouseWheelCallback = (deltaY: number, deltaX: number) => void;
export const useScreenMouseWheel = (callback: ScreenMouseWheelCallback) => {
const registerScreenMouseWheelCallback = inject<
(callback: ScreenMouseWheelCallback) => () => void
>("registerScreenMouseWheelCallback");
onMounted(() => {
if (!registerScreenMouseWheelCallback) {
throw new Error(
"Missing registerScreenMouseWheelCallback - useScreenMouseWheel must be used within a Screen component",
);
}
const unregister = registerScreenMouseWheelCallback(callback);
onUnmounted(unregister);
});
};