feat: nds render system
This commit is contained in:
20
app/app.vue
Normal file
20
app/app.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import Background from "./components/screen/Background.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<Screen>
|
||||
<Background />
|
||||
<ScreenStats />
|
||||
</Screen>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
border: 1px solid red;
|
||||
}
|
||||
</style>
|
||||
6
app/components/screen/Background.vue
Normal file
6
app/components/screen/Background.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
useRender((ctx) => {
|
||||
ctx.fillStyle = "green";
|
||||
ctx.fillRect(0, 0, 100, 100);
|
||||
});
|
||||
</script>
|
||||
70
app/components/screen/Screen.vue
Normal file
70
app/components/screen/Screen.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
const canvas = useTemplateRef("canvas");
|
||||
|
||||
const updateCallbacks = new Set<UpdateCallback>();
|
||||
const renderCallbacks = new Set<RenderCallback>();
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animationFrameId: number | null = null;
|
||||
let lastFrameTime = 0;
|
||||
let lastRealFrameTime = 0;
|
||||
|
||||
const registerUpdateCallback = (callback: UpdateCallback) => {
|
||||
updateCallbacks.add(callback);
|
||||
return () => updateCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const registerRenderCallback = (callback: RenderCallback) => {
|
||||
renderCallbacks.add(callback);
|
||||
return () => renderCallbacks.delete(callback);
|
||||
};
|
||||
|
||||
const renderFrame = (timestamp: number) => {
|
||||
if (!ctx) return;
|
||||
|
||||
const deltaTime = timestamp - lastFrameTime;
|
||||
lastFrameTime = timestamp;
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
// update
|
||||
for (const callback of updateCallbacks) {
|
||||
callback(deltaTime, lastRealFrameTime);
|
||||
}
|
||||
|
||||
// render
|
||||
ctx.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
|
||||
for (const callback of renderCallbacks) {
|
||||
callback(ctx);
|
||||
}
|
||||
|
||||
lastRealFrameTime = Date.now() - start;
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvas.value) throw new Error("Missing canvas");
|
||||
|
||||
ctx = canvas.value.getContext("2d");
|
||||
if (!ctx) throw new Error("Missing 2d context");
|
||||
|
||||
provide("registerUpdateCallback", registerUpdateCallback);
|
||||
provide("registerRenderCallback", registerRenderCallback);
|
||||
|
||||
animationFrameId = requestAnimationFrame(renderFrame);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrameId !== null) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="canvas" :width="SCREEN_WIDTH" :height="SCREEN_HEIGHT" />
|
||||
|
||||
<slot v-if="canvas" />
|
||||
</template>
|
||||
68
app/components/screen/Stats.vue
Normal file
68
app/components/screen/Stats.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const SAMPLES = 60;
|
||||
let average = { deltaTime: 0, realDeltaTime: 0 };
|
||||
const lastFrames: (typeof average)[] = [];
|
||||
|
||||
useUpdate((deltaTime, realDeltaTime) => {
|
||||
lastFrames.push({ deltaTime, realDeltaTime });
|
||||
|
||||
if (lastFrames.length > SAMPLES) {
|
||||
lastFrames.shift();
|
||||
}
|
||||
|
||||
if (lastFrames.length > 0) {
|
||||
average = {
|
||||
deltaTime:
|
||||
lastFrames.reduce((acc, v) => acc + v.deltaTime, 0) /
|
||||
lastFrames.length,
|
||||
realDeltaTime:
|
||||
lastFrames.reduce((acc, v) => acc + v.realDeltaTime, 0) /
|
||||
lastFrames.length,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useRender((ctx) => {
|
||||
const LINE_COUNT = 5;
|
||||
const LINE_HEIGHT = 12;
|
||||
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fillRect(props.x - 2, props.y, 140, LINE_COUNT * LINE_HEIGHT + 3);
|
||||
|
||||
let textY = props.y;
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillText("[avg on 60 frames]", props.x, (textY += LINE_HEIGHT));
|
||||
ctx.fillText(
|
||||
`fps=${(1000 / average.deltaTime).toFixed()}`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`frame_time=${average.deltaTime.toFixed(2)}ms`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`real_fps=${(1000 / average.realDeltaTime).toFixed()}`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
ctx.fillText(
|
||||
`real_frame_time=${average.realDeltaTime.toFixed(2)}ms`,
|
||||
props.x,
|
||||
(textY += LINE_HEIGHT),
|
||||
);
|
||||
});
|
||||
</script>
|
||||
18
app/composables/useRender.ts
Normal file
18
app/composables/useRender.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type RenderCallback = (ctx: CanvasRenderingContext2D) => 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);
|
||||
});
|
||||
};
|
||||
18
app/composables/useUpdate.ts
Normal file
18
app/composables/useUpdate.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type UpdateCallback = (deltaTime: number, realFrameTime: number) => void;
|
||||
|
||||
export const useUpdate = (callback: UpdateCallback) => {
|
||||
const registerUpdateCallback = inject<
|
||||
(callback: UpdateCallback) => () => void
|
||||
>("registerUpdateCallback");
|
||||
|
||||
onMounted(() => {
|
||||
if (!registerUpdateCallback) {
|
||||
throw new Error(
|
||||
"Missing registerUpdateCallback - useUpdate must be used within a Screen component",
|
||||
);
|
||||
}
|
||||
|
||||
const unregister = registerUpdateCallback(callback);
|
||||
onUnmounted(unregister);
|
||||
});
|
||||
};
|
||||
2
app/utils/screen.ts
Normal file
2
app/utils/screen.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const SCREEN_WIDTH = 256;
|
||||
export const SCREEN_HEIGHT = 192;
|
||||
Reference in New Issue
Block a user