01
WATER_CUBE
DRAG TO ORBIT // HOLD THE SAILBOAT DOWN, YOU CANNOT SINK IT
HOW IT'S MADE [+]
<template>
<div class="oasis-wrap">
<canvas ref="canvasRef" class="oasis-canvas"></canvas>
<p v-if="loading" class="oasis-loading font-mono" role="status">LOADING_SCENE<span class="oasis-caret">_</span></p>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue"
// @ts-ignore — runtime ships its own types but vite resolves the browser build
import { Application } from "@splinetool/runtime"
// Spline water-cube scene, self-hosted from /oasis/scene.splinecode so no
// request ever leaves this domain. The runtime drives its own render loop.
const props = withDefaults(defineProps<{ scene?: string }>(), {
scene: "/oasis/scene.splinecode",
})
const canvasRef = ref<HTMLCanvasElement | null>(null)
const loading = ref(true)
let app: InstanceType<typeof Application> | null = null
onMounted(async () => {
// wasmPath keeps the runtime's wasm on this domain too (default is unpkg)
app = new Application(canvasRef.value!, { wasmPath: "/oasis/wasm" })
try {
await app.load(props.scene)
} finally {
loading.value = false
}
})
onBeforeUnmount(() => {
app?.dispose()
app = null
})
</script>
<style scoped>
.oasis-wrap {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.oasis-canvas {
width: 100%;
height: 100%;
display: block;
touch-action: none;
}
.oasis-loading {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-size: 0.62rem; font-weight: 700; letter-spacing: 0.24em;
color: rgba(27, 27, 27, 0.5);
pointer-events: none;
}
.oasis-caret { animation: oasis-blink 1s steps(1) infinite; }
@keyframes oasis-blink { 50% { opacity: 0; } }
</style>
The scene lives at /oasis/scene.splinecode (310 KB, self-hosted). The npm runtime drives its own render loop against one canvas, the Vue side is a mount, a load and a dispose.