styles & new model

This commit is contained in:
T-A-H-prog
2026-09-12 20:45:20 +02:00
parent e33c509856
commit 1459fd3d2f
7 changed files with 1269 additions and 243 deletions

View File

@@ -0,0 +1,376 @@
<script setup lang="js">
import { nextTick, onMounted, onUnmounted, ref } from "vue";
const code = ref("");
const loggedIn = ref(false);
const error = ref("");
const inputRef = ref(null);
function cleanCode(event) {
code.value = event.target.value.replace(/\D/g, "").slice(0, 4);
error.value = "";
}
function login() {
if (code.value === "1337") {
loggedIn.value = true;
error.value = "";
return;
}
code.value = "";
error.value = "ACCESS DENIED";
nextTick(() => inputRef.value?.focus());
}
function logout() {
loggedIn.value = false;
code.value = "";
error.value = "";
nextTick(() => inputRef.value?.focus());
}
function leaveComputer() {
if (window.parent === window) return;
window.parent.postMessage({ type: "leave-computer" }, window.location.origin);
}
function onKeydown(event) {
if (event.key === "Escape") leaveComputer();
}
onMounted(() => {
window.addEventListener("keydown", onKeydown);
document.documentElement.addEventListener("mouseleave", leaveComputer);
inputRef.value?.focus();
});
onUnmounted(() => {
window.removeEventListener("keydown", onKeydown);
document.documentElement.removeEventListener("mouseleave", leaveComputer);
});
</script>
<template>
<main class="page">
<header>
<b>FEDERAL RECORDS SYSTEM</b>
<span>SECURE SERVER 04</span>
</header>
<section v-if="!loggedIn" class="login-box">
<h1>Restricted Access</h1>
<p>This system is for authorized personnel only.</p>
<form @submit.prevent="login">
<label for="code">Access Code:</label>
<input
id="code"
ref="inputRef"
:value="code"
type="password"
inputmode="numeric"
maxlength="4"
autocomplete="off"
@input="cleanCode"
/>
<button type="submit">LOGIN</button>
</form>
<p v-if="error" class="error" role="alert">{{ error }}</p>
<p class="help">Press ESC to leave terminal.</p>
</section>
<section v-else class="records">
<nav>
<b>RECORD DATABASE</b>
<button type="button" @click="logout">LOG OUT</button>
</nav>
<h1>Case File 04-1337</h1>
<table>
<tbody>
<tr>
<th>Status</th>
<td>OPEN</td>
</tr>
<tr>
<th>Classification</th>
<td>RESTRICTED</td>
</tr>
<tr>
<th>Subject</th>
<td>UNKNOWN</td>
</tr>
<tr>
<th>Last activity</th>
<td>03:17 / SECTOR 12</td>
</tr>
</tbody>
</table>
<div class="note">
<b>REPORT</b>
<p>
An unidentified signal was recorded near the archive terminal.
Investigation remains active. No further action authorized.
</p>
</div>
<footer>FICTIONAL ARCHIVE SYSTEM // LOCAL TERMINAL</footer>
</section>
</main>
</template>
<style scoped>
:global(*) {
box-sizing: border-box;
}
:global(html),
:global(body),
:global(#app) {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #000;
}
:global(body) {
font-family: Verdana, Arial, sans-serif;
}
.page {
width: 100%;
height: 100dvh;
overflow: hidden;
color: #b7c2b9;
background: #000;
font-size: clamp(12px, 1.4vw, 16px);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
height: 54px;
padding: 0 22px;
border-bottom: 1px solid #526056;
color: #d4ddd6;
background: #111;
font-family: "Courier New", monospace;
}
header span {
color: #718076;
font-size: 0.75em;
}
.login-box {
width: min(430px, calc(100% - 36px));
margin: clamp(40px, 14vh, 110px) auto 0;
padding: 22px;
border: 1px solid #58635b;
background: #080a08;
}
h1 {
margin: 0 0 18px;
color: #d6ddd7;
font:
bold 1.25em "Courier New",
monospace;
}
p {
line-height: 1.5;
}
form {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
margin-top: 24px;
}
label {
white-space: nowrap;
}
input,
button {
border: 1px solid #5c685e;
border-radius: 0;
font: inherit;
}
input {
min-width: 0;
padding: 7px;
outline: none;
color: #9ee6ad;
background: #000;
}
input:focus {
border-color: #9ee6ad;
}
button {
padding: 7px 12px;
color: #d1d8d2;
background: #242924;
cursor: pointer;
}
button:hover,
button:focus-visible {
color: #fff;
background: #343b35;
}
.error {
margin: 15px 0 0;
color: #d76b62;
font-family: "Courier New", monospace;
}
.help {
margin: 22px 0 0;
color: #667068;
font-size: 0.75em;
}
.records {
width: min(760px, calc(100% - 36px));
height: calc(100dvh - 82px);
margin: 18px auto 0;
overflow: hidden;
border: 1px solid #4d574f;
background: #060706;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
height: 42px;
padding: 0 14px;
border-bottom: 1px solid #4d574f;
color: #9ee6ad;
background: #121512;
font-family: "Courier New", monospace;
}
nav button {
padding: 4px 9px;
font-size: 0.75em;
}
.records > h1,
.records > table,
.records > .note {
margin-right: 20px;
margin-left: 20px;
}
.records > h1 {
margin-top: 20px;
}
table {
width: calc(100% - 40px);
border-collapse: collapse;
}
th,
td {
padding: 8px 10px;
border: 1px solid #3e473f;
text-align: left;
}
th {
width: 34%;
color: #78847a;
background: #0c0f0c;
font-weight: normal;
}
td {
color: #a7d9b0;
font-family: "Courier New", monospace;
}
.note {
margin-top: 20px;
padding: 14px;
border: 1px solid #3e473f;
}
.note b {
color: #78847a;
font-size: 0.8em;
}
.note p {
margin: 8px 0 0;
}
footer {
margin: 22px 20px 0;
color: #566058;
font:
0.7em "Courier New",
monospace;
text-align: right;
}
@media (max-width: 500px), (max-height: 500px) {
header {
height: 44px;
padding: 0 12px;
}
.login-box {
margin-top: 24px;
padding: 16px;
}
form {
grid-template-columns: 1fr auto;
}
form label {
grid-column: 1 / -1;
}
.records {
height: calc(100dvh - 58px);
margin-top: 7px;
}
.records > h1 {
margin-top: 12px;
margin-bottom: 10px;
}
th,
td {
padding: 5px 7px;
}
.note {
margin-top: 10px;
padding: 8px;
}
footer {
display: none;
}
}
</style>

Binary file not shown.

View File

@@ -111,8 +111,6 @@ function loadModel() {
model.position.set(0, 0, 0); model.position.set(0, 0, 0);
scene.add(model); scene.add(model);
gsapCtx = scrollCamreaGsap(center); gsapCtx = scrollCamreaGsap(center);
document.title = "Model geladen";
}, },
(progress) => { (progress) => {
const percent = ((progress.loaded / progress.total) * 100).toFixed(1); const percent = ((progress.loaded / progress.total) * 100).toFixed(1);

View File

@@ -3,8 +3,6 @@ import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three"; import * as THREE from "three";
import gsap from "gsap"; import gsap from "gsap";
import OsWebseite from "../computer/OsWebseite.vue";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
@@ -38,6 +36,9 @@ let iframeObject;
let computerMode = false; let computerMode = false;
let cameraAnimation = null; let cameraAnimation = null;
let iframeInteractionReady = false;
let iframePointerWasInside = false;
let iframeReadyTimer = null;
const originalCameraPosition = new THREE.Vector3(); const originalCameraPosition = new THREE.Vector3();
const originalControlsTarget = new THREE.Vector3(); const originalControlsTarget = new THREE.Vector3();
@@ -75,14 +76,14 @@ function initThree(canvas, cssContainer) {
cssContainer.appendChild(cssRenderer.domElement); cssContainer.appendChild(cssRenderer.domElement);
// scene.add(new THREE.AmbientLight(0xffffff, 0.02)); // scene.add(new THREE.AmbientLight(0xffffff, 0.102));
loadModel(); loadModel();
gsap.ticker.add(render); gsap.ticker.add(render);
} }
function loadModel() { function loadModel() {
const modelUrl = "/models/book_shelf_pc_draco-v1.glb"; const modelUrl = "/models/book_shelf-v1.glb";
const dracoLoader = new DRACOLoader(); const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/"); dracoLoader.setDecoderPath("/draco/");
@@ -122,7 +123,7 @@ function loadModel() {
// camera.lookAt(center); // camera.lookAt(center);
camera.position.set( camera.position.set(
center.x, center.x,
center.y + distance * 0.35, // etwas von oben center.y + distance * 0.35,
center.z + distance, center.z + distance,
); );
@@ -147,8 +148,6 @@ function loadModel() {
bulb.add(pointLight); bulb.add(pointLight);
} }
document.title = "Model geladen";
}, },
(progress) => { (progress) => {
if (progress.total) { if (progress.total) {
@@ -186,6 +185,9 @@ function enterComputerMode() {
if (!screenMesh || computerMode) return; if (!screenMesh || computerMode) return;
computerMode = true; computerMode = true;
iframeInteractionReady = false;
iframePointerWasInside = false;
window.clearTimeout(iframeReadyTimer);
originalCameraPosition.copy(camera.position); originalCameraPosition.copy(camera.position);
originalControlsTarget.copy(controls.target); originalControlsTarget.copy(controls.target);
@@ -198,7 +200,7 @@ function enterComputerMode() {
const screenQuaternion = new THREE.Quaternion(); const screenQuaternion = new THREE.Quaternion();
screenMesh.getWorldQuaternion(screenQuaternion); screenMesh.getWorldQuaternion(screenQuaternion);
const cameraOffset = new THREE.Vector3(0, 0.3, 2.9); const cameraOffset = new THREE.Vector3(0, 2, 0);
cameraOffset.applyQuaternion(screenQuaternion); cameraOffset.applyQuaternion(screenQuaternion);
@@ -216,10 +218,15 @@ function enterComputerMode() {
iframe.focus(); iframe.focus();
try { try {
iframe.contentDocument?.querySelector("#password")?.focus(); iframe.contentDocument?.querySelector("input")?.focus();
} catch (error) { } catch (error) {
console.warn("iframe konnte nicht fokussiert werden", error); console.warn("iframe konnte nicht fokussiert werden", error);
} }
// Verhindert, dass ein Lade-/Layout-event sofort wieder herauszoomt.
iframeReadyTimer = window.setTimeout(() => {
iframeInteractionReady = true;
}, 300);
}, },
}); });
@@ -247,16 +254,20 @@ function enterComputerMode() {
0, 0,
); );
} }
function leaveComputerMode() { function leaveComputerMode() {
if (!computerMode) return; if (!computerMode) return;
computerMode = false; computerMode = false;
iframeInteractionReady = false;
iframePointerWasInside = false;
window.clearTimeout(iframeReadyTimer);
iframe.style.pointerEvents = "none"; iframe.style.pointerEvents = "none";
iframe.blur(); iframe.blur();
// Beim Herauszoomen wieder ausschließlich das GLTF-Display zeigen. // Noch sichtbar lassen während der Kamera-Animation
iframeObject.visible = false; iframeObject.visible = true;
screenMesh.visible = true; screenMesh.visible = true;
cameraAnimation?.kill(); cameraAnimation?.kill();
@@ -268,6 +279,9 @@ function leaveComputerMode() {
onComplete() { onComplete() {
controls.enabled = true; controls.enabled = true;
controls.update(); controls.update();
// Erst nach dem Herauszoomen ausblenden
iframeObject.visible = false;
}, },
}); });
@@ -295,11 +309,13 @@ function leaveComputerMode() {
0, 0,
); );
} }
function onIframeMessage(event) { function onIframeMessage(event) {
if (event.source !== iframe?.contentWindow) return; if (event.source !== iframe?.contentWindow) return;
if (event.origin !== window.location.origin) return; if (event.origin !== window.location.origin) return;
if (event.data?.type === "leave-computer") { if (event.data?.type === "leave-computer") {
if (!computerMode || !iframeInteractionReady) return;
leaveComputerMode(); leaveComputerMode();
} }
} }
@@ -336,8 +352,8 @@ function onResize() {
} }
function createComputerScreen(mesh) { function createComputerScreen(mesh) {
const iframeWidth = 1280; const iframeWidth = 1480;
const iframeHeight = 720; const iframeHeight = 920;
const container = document.createElement("div"); const container = document.createElement("div");
@@ -347,10 +363,8 @@ function createComputerScreen(mesh) {
container.style.overflow = "hidden"; container.style.overflow = "hidden";
iframe = document.createElement("iframe"); iframe = document.createElement("iframe");
const parentOrigin = JSON.stringify(window.location.origin);
// Eigenständige Oberfläche: keine zusätzliche Vue-Route/HTML-Datei nötig. iframe.src = `${window.location.origin}/motion/computer`;
iframe.src = "/computer";
iframe.title = "Computer"; iframe.title = "Computer";
iframe.tabIndex = 0; iframe.tabIndex = 0;
@@ -361,15 +375,24 @@ function createComputerScreen(mesh) {
container.appendChild(iframe); container.appendChild(iframe);
// Zusätzlicher Fallback am iframe-Element selbst. // Erst ein echtes Betreten und anschließendes Verlassen schließt den Modus.
iframe.addEventListener("mouseleave", () => { iframe.addEventListener("pointerenter", () => {
if (computerMode) leaveComputerMode(); if (!computerMode) return;
iframePointerWasInside = true;
});
iframe.addEventListener("pointerleave", () => {
if (!computerMode || !iframeInteractionReady || !iframePointerWasInside) {
return;
}
leaveComputerMode();
}); });
iframe.addEventListener("load", () => { iframe.addEventListener("load", () => {
try { try {
iframe.contentWindow.addEventListener("keydown", (event) => { iframe.contentWindow.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape" && computerMode && iframeInteractionReady) {
leaveComputerMode(); leaveComputerMode();
} }
}); });
@@ -386,7 +409,6 @@ function createComputerScreen(mesh) {
updateIframeTransform(mesh, iframeWidth, iframeHeight); updateIframeTransform(mesh, iframeWidth, iframeHeight);
} }
function updateIframeTransform(mesh, iframeWidth, iframeHeight) { function updateIframeTransform(mesh, iframeWidth, iframeHeight) {
mesh.updateWorldMatrix(true, false); mesh.updateWorldMatrix(true, false);
@@ -406,19 +428,34 @@ function updateIframeTransform(mesh, iframeWidth, iframeHeight) {
iframeObject.position.copy(worldPosition); iframeObject.position.copy(worldPosition);
iframeObject.quaternion.copy(worldQuaternion); iframeObject.quaternion.copy(worldQuaternion);
/*
* Korrektur für das neue Screen-Mesh.
* Bei falscher Richtung -90 durch +90 ersetzen.
*/
iframeObject.rotateX(THREE.MathUtils.degToRad(-90));
iframeObject.rotateZ(THREE.MathUtils.degToRad(-90));
iframeObject.translateX(-0.04);
iframeObject.translateY(+0.013);
iframeObject.translateZ(-0.1);
const screenWidth = Math.abs(meshSize.x * worldScale.x) + 0.03;
const screenHeight = Math.abs(meshSize.y * worldScale.y) + 0.61;
iframeObject.scale.set( iframeObject.scale.set(
(meshSize.x * worldScale.x + 0.03) / iframeWidth, screenWidth / iframeWidth,
(meshSize.y * worldScale.y + 0.03) / iframeHeight, screenHeight / iframeHeight,
1, 1,
); );
// Etwas vor den eigentlichen Bildschirm setzen, /*
// damit es nicht genau auf derselben Ebene liegt. * Vorderseite nach der Korrekturrotation berechnen.
const normal = new THREE.Vector3(0, 0, -1.4) */
.applyQuaternion(worldQuaternion) const iframeNormal = new THREE.Vector3(0, 0, 1)
.applyQuaternion(iframeObject.quaternion)
.normalize(); .normalize();
iframeObject.position.addScaledVector(normal, -0.2); iframeObject.position.addScaledVector(iframeNormal, 0.1);
} }
let resizeObserver; let resizeObserver;
@@ -460,6 +497,7 @@ onUnmounted(() => {
resizeObserver?.disconnect(); resizeObserver?.disconnect();
cameraAnimation?.kill(); cameraAnimation?.kill();
window.clearTimeout(iframeReadyTimer);
controls?.dispose(); controls?.dispose();
renderer?.dispose(); renderer?.dispose();

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ const routes: RouteRecordRaw[] = [
{path: '/motion/me', component: BigMeImg}, {path: '/motion/me', component: BigMeImg},
{path: '/books', component: BooksShow}, {path: '/books', component: BooksShow},
{ {
path: "/computer", component: OsWebseite}, path: "/motion/computer", component: OsWebseite},
// { path: '/ducks', // { path: '/ducks',
// component: DucksInfo, // component: DucksInfo,