feat: add embed for place game

This commit is contained in:
2026-09-17 01:51:38 +02:00
parent 9fc9e19d68
commit e38b610456
4 changed files with 75 additions and 4 deletions
+9 -4
View File
@@ -1,11 +1,16 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { RouterView } from "vue-router";
import { computed, onMounted } from "vue";
import { RouterView, useRoute } from "vue-router";
import AppHeader from "@/layout/AppHeader.vue";
import AppFooter from "@/layout/AppFooter.vue";
import { useUserStore } from "@/stores/user";
const userStore = useUserStore();
const route = useRoute();
// Embed routes (e.g. place/embed) are meant to be dropped into an <iframe>
// as-is - the header/footer chrome would just eat into the fixed space a
// stream overlay or OBS browser source gives it.
const isEmbed = computed(() => route.meta.embed === true);
onMounted(() => {
userStore.loadUserData();
@@ -14,10 +19,10 @@ onMounted(() => {
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<AppHeader v-if="!isEmbed" />
<main class="flex-1 flex flex-col">
<RouterView />
</main>
<AppFooter />
<AppFooter v-if="!isEmbed" />
</div>
</template>
+42
View File
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { usePlaceSocket } from "@/composables/usePlaceSocket";
import { PALETTE } from "@/palette";
interface Props {
eventPath: string;
}
const props = defineProps<Props>();
// Read-only: fed straight into an <iframe> elsewhere (a stream overlay, a
// README, another site), so there's no header, palette, or click handling
// here - just the live canvas. Reachable without login (see place_embed in
// urls.py) since an OBS browser source can't do the CAS SSO login flow.
const canvasRef = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;
function drawPixel(x: number, y: number, colorIndex: number) {
if (!ctx) return;
ctx.fillStyle = PALETTE[colorIndex];
ctx.fillRect(x, y, 1, 1);
}
const { connected, gridWidth, gridHeight } = usePlaceSocket(props.eventPath, drawPixel);
onMounted(() => {
ctx = canvasRef.value?.getContext("2d") ?? null;
});
</script>
<template>
<div class="w-screen h-screen bg-base-200 flex items-center justify-center overflow-hidden">
<span class="badge badge-xs absolute top-2 left-2 opacity-70"
:class="connected ? 'badge-success' : 'badge-error'" />
<canvas ref="canvasRef" :width="gridWidth" :height="gridHeight" :style="{
width: 'min(100vw, 100vh)',
aspectRatio: `${gridWidth} / ${gridHeight}`,
imageRendering: 'pixelated',
}" />
</div>
</template>
+11
View File
@@ -5,6 +5,7 @@ import Market from "@/Market.vue";
import Noita from "@/Noita.vue";
import OpusMagnum from "@/OpusMagnum.vue";
import Place from "@/Place.vue";
import PlaceEmbed from "@/PlaceEmbed.vue";
import Submissions from "@/Submissions.vue";
import SubmissionsSelect from "@/SubmissionsSelect.vue";
import { usePageHeaderStore } from "@/stores/pageHeader";
@@ -16,6 +17,9 @@ declare module "vue-router" {
// usePageHeaderStore() instead and leave this out.
titleKey?: string;
getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw;
// Skips AppHeader/AppFooter (see App.vue) - for pages meant to be
// dropped into an <iframe> as-is, like place/embed.
embed?: boolean;
}
}
@@ -68,6 +72,13 @@ export const router = createRouter({
props: true,
meta: { titleKey: "place.header", getBackRoute: eventHome },
},
{
path: "/:eventPath/place/embed",
name: "place-embed",
component: PlaceEmbed,
props: true,
meta: { embed: true },
},
{
path: "/:eventPath/submissions/:gamePath",
name: "submissions-select",