LAB_07 // THE_OASIS

THE
OASIS

Spline // Self-Hosted Scene // Drag To Orbit

A cubic ocean to calm down in: a sailboat on a caustic water surface, kelp and fish below, clouds above. Modeled in Spline, rendered by its runtime, and the whole scene ships from this domain, no calls home.

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.