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
@@ -75,6 +75,18 @@ def place_home(request: HttpRequest, event_path: str):
return render(request, "app.html") return render(request, "app.html")
# Deliberately not behind @login_required: this is meant to be dropped into
# an <iframe> (an OBS browser source, a stream overlay) that can't do the
# CAS SSO login flow. It's read-only anyway - placing a pixel still requires
# a logged-in websocket connection (see place/consumers.py) - so the only
# thing this exposes to anonymous viewers is the live canvas itself, same as
# watching the non-embed page already allows.
@require_game_enabled(path="place")
def place_embed(request: HttpRequest, event_path: str):
get_object_or_404(Event, path=event_path)
return render(request, "app.html")
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"), path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
@@ -85,6 +97,7 @@ urlpatterns = [
path("<slug:event_path>/noita", noita_home, name="noita.home"), path("<slug:event_path>/noita", noita_home, name="noita.home"),
path("<slug:event_path>/clock", clock_home, name="clock.home"), path("<slug:event_path>/clock", clock_home, name="clock.home"),
path("<slug:event_path>/place", place_home, name="place.home"), path("<slug:event_path>/place", place_home, name="place.home"),
path("<slug:event_path>/place/embed", place_embed, name="place.embed"),
path( path(
"<slug:event_path>/submissions/<slug:game_path>", "<slug:event_path>/submissions/<slug:game_path>",
submission_home, submission_home,
+9 -4
View File
@@ -1,11 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { computed, onMounted } from "vue";
import { RouterView } from "vue-router"; import { RouterView, useRoute } from "vue-router";
import AppHeader from "@/layout/AppHeader.vue"; import AppHeader from "@/layout/AppHeader.vue";
import AppFooter from "@/layout/AppFooter.vue"; import AppFooter from "@/layout/AppFooter.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
const userStore = useUserStore(); 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(() => { onMounted(() => {
userStore.loadUserData(); userStore.loadUserData();
@@ -14,10 +19,10 @@ onMounted(() => {
<template> <template>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<AppHeader /> <AppHeader v-if="!isEmbed" />
<main class="flex-1 flex flex-col"> <main class="flex-1 flex flex-col">
<RouterView /> <RouterView />
</main> </main>
<AppFooter /> <AppFooter v-if="!isEmbed" />
</div> </div>
</template> </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 Noita from "@/Noita.vue";
import OpusMagnum from "@/OpusMagnum.vue"; import OpusMagnum from "@/OpusMagnum.vue";
import Place from "@/Place.vue"; import Place from "@/Place.vue";
import PlaceEmbed from "@/PlaceEmbed.vue";
import Submissions from "@/Submissions.vue"; import Submissions from "@/Submissions.vue";
import SubmissionsSelect from "@/SubmissionsSelect.vue"; import SubmissionsSelect from "@/SubmissionsSelect.vue";
import { usePageHeaderStore } from "@/stores/pageHeader"; import { usePageHeaderStore } from "@/stores/pageHeader";
@@ -16,6 +17,9 @@ declare module "vue-router" {
// usePageHeaderStore() instead and leave this out. // usePageHeaderStore() instead and leave this out.
titleKey?: string; titleKey?: string;
getBackRoute?: (route: RouteLocationNormalizedLoaded) => RouteLocationRaw; 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, props: true,
meta: { titleKey: "place.header", getBackRoute: eventHome }, meta: { titleKey: "place.header", getBackRoute: eventHome },
}, },
{
path: "/:eventPath/place/embed",
name: "place-embed",
component: PlaceEmbed,
props: true,
meta: { embed: true },
},
{ {
path: "/:eventPath/submissions/:gamePath", path: "/:eventPath/submissions/:gamePath",
name: "submissions-select", name: "submissions-select",