This commit is contained in:
T-A-H-prog
2026-09-10 21:07:05 +02:00
parent 6eaa10e34f
commit e33c509856
7 changed files with 752 additions and 98 deletions

View File

@@ -25,7 +25,6 @@ body,
body {
font-family: Inter, system-ui, sans-serif;
background: #0f0f0f;
color: white;
overflow-x: hidden;
cursor:

View File

@@ -2,62 +2,82 @@
import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three";
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
import OsWebseite from "../computer/OsWebseite.vue";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
CSS3DRenderer,
CSS3DObject,
} from "three/examples/jsm/renderers/CSS3DRenderer.js";
import ski_video from "/vids/th.mp4";
import normal_texture from "../../assets/myself/terminalretro.jpg";
defineOptions({ inheritAttrs: false });
const canvasRef = ref(null);
let renderer, scene, camera, model, controls, gsapCtx;
const cssRendererRef = ref(null);
let renderer;
let cssRenderer;
let scene;
let cssScene;
let camera;
let model;
let controls;
let gsapCtx;
let raycaster;
let mouse;
let screens;
let screenMesh;
const video_ski = document.createElement("video");
let iframe;
let iframeObject;
video_ski.src = ski_video;
video_ski.loop = true;
video_ski.muted = true;
let computerMode = false;
let cameraAnimation = null;
function initThree(canvas) {
const originalCameraPosition = new THREE.Vector3();
const originalControlsTarget = new THREE.Vector3();
function initThree(canvas, cssContainer) {
renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
});
renderer.setPixelRatio(1); //TODO
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
scene = new THREE.Scene();
cssScene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(23, 1, 0.1, 140);
// const light = new THREE.DirectionalLight(0xffe0b0, 0.9);
// light.position.set(-1, 4, 5);
// scene.add(light);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;
controls.enableDamping = true;
// controls.enabled = false;
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
// controls.minDistance = 5;
// controls.maxDistance = 5;
controls.maxPolarAngle = Math.PI / 2;
controls.minAzimuthAngle = -Math.PI / 2;
controls.maxAzimuthAngle = Math.PI / 2;
scene.add(new THREE.AmbientLight(0xffffff, 0.02));
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
cssRenderer = new CSS3DRenderer();
cssRenderer.domElement.className = "css-renderer";
cssRenderer.domElement.style.position = "absolute";
cssRenderer.domElement.style.inset = "0";
cssContainer.appendChild(cssRenderer.domElement);
// scene.add(new THREE.AmbientLight(0xffffff, 0.02));
loadModel();
gsap.ticker.add(render);
}
@@ -87,7 +107,15 @@ function loadModel() {
// Kamera passend zum Objekt positionieren
const distance = maxSize * 2;
screen = model.getObjectByName("fernseher1Bild");
screenMesh =
model.getObjectByName("screen") ??
model.getObjectByName("fernseher1Bild");
if (screenMesh) {
createComputerScreen(screenMesh);
} else {
console.warn("Kein Bildschirm-Mesh gefunden");
}
// camera.position.set(center.x, center.y, center.z + distance);
@@ -106,11 +134,6 @@ function loadModel() {
// OrbitControls auf das Objekt ausrichten
controls.target.copy(center);
controls.update();
console.log("Model geladen:", model);
console.log("Center:", center);
console.log("Size:", size);
const bulb = model.getObjectByName("light");
if (bulb) {
@@ -130,6 +153,7 @@ function loadModel() {
(progress) => {
if (progress.total) {
const percent = ((progress.loaded / progress.total) * 100).toFixed(1);
document.title = percent;
console.log(`Loading: ${percent}%`);
}
},
@@ -140,103 +164,321 @@ function loadModel() {
);
}
const screen_on = ref(false);
function onCanvasClick(event) {
if (!screenMesh || computerMode) return;
const rect = canvasRef.value.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const screen = model?.getObjectByName("screen");
if (!screen) return;
const intersects = raycaster.intersectObject(screen, true);
const texure_ski = new THREE.VideoTexture(video_ski);
texure_ski.flipY = false;
// texure_ski.offset.set(0.3, 2);
// texure_ski.center.set(0.5, 0.5);
texure_ski.repeat.set(4.3, 4.3);
texure_ski.offset.set(-0.3, -0.33);
const normalTexture = new THREE.TextureLoader().load(normal_texture);
normalTexture.flipY = false;
normalTexture.repeat.set(5.6, 5.6);
normalTexture.offset.set(-0.54, -0.5);
const intersects = raycaster.intersectObject(screenMesh, true);
if (intersects.length > 0) {
if (screen_on.value) {
screen.material = new THREE.MeshBasicMaterial({
map: normalTexture,
});
video_ski.pause();
} else {
screen.material = new THREE.MeshBasicMaterial({
map: texure_ski,
});
video_ski.play();
}
screen_on.value = !screen_on.value;
enterComputerMode();
}
}
function enterComputerMode() {
if (!screenMesh || computerMode) return;
computerMode = true;
originalCameraPosition.copy(camera.position);
originalControlsTarget.copy(controls.target);
controls.enabled = false;
const screenPosition = new THREE.Vector3();
screenMesh.getWorldPosition(screenPosition);
const screenQuaternion = new THREE.Quaternion();
screenMesh.getWorldQuaternion(screenQuaternion);
const cameraOffset = new THREE.Vector3(0, 0.3, 2.9);
cameraOffset.applyQuaternion(screenQuaternion);
const targetCameraPosition = screenPosition.clone().add(cameraOffset);
cameraAnimation?.kill();
cameraAnimation = gsap.timeline({
onUpdate() {
camera.lookAt(controls.target);
},
onComplete() {
screenMesh.visible = true;
iframeObject.visible = true;
iframe.style.pointerEvents = "auto";
iframe.focus();
try {
iframe.contentDocument?.querySelector("#password")?.focus();
} catch (error) {
console.warn("iframe konnte nicht fokussiert werden", error);
}
},
});
cameraAnimation.to(
camera.position,
{
x: targetCameraPosition.x,
y: targetCameraPosition.y,
z: targetCameraPosition.z,
duration: 1.3,
ease: "power3.inOut",
},
0,
);
cameraAnimation.to(
controls.target,
{
x: screenPosition.x,
y: screenPosition.y,
z: screenPosition.z,
duration: 1.3,
ease: "power3.inOut",
},
0,
);
}
function leaveComputerMode() {
if (!computerMode) return;
computerMode = false;
iframe.style.pointerEvents = "none";
iframe.blur();
// Beim Herauszoomen wieder ausschließlich das GLTF-Display zeigen.
iframeObject.visible = false;
screenMesh.visible = true;
cameraAnimation?.kill();
cameraAnimation = gsap.timeline({
onUpdate() {
camera.lookAt(controls.target);
},
onComplete() {
controls.enabled = true;
controls.update();
},
});
cameraAnimation.to(
camera.position,
{
x: originalCameraPosition.x,
y: originalCameraPosition.y,
z: originalCameraPosition.z,
duration: 1,
ease: "power3.inOut",
},
0,
);
cameraAnimation.to(
controls.target,
{
x: originalControlsTarget.x,
y: originalControlsTarget.y,
z: originalControlsTarget.z,
duration: 1,
ease: "power3.inOut",
},
0,
);
}
function onIframeMessage(event) {
if (event.source !== iframe?.contentWindow) return;
if (event.origin !== window.location.origin) return;
if (event.data?.type === "leave-computer") {
leaveComputerMode();
}
}
function render() {
if (!renderer) return;
if (!renderer || !scene || !camera) return;
controls.update();
if (!computerMode) {
controls?.update();
}
renderer.render(scene, camera);
if (cssRenderer && cssScene) {
cssRenderer.render(cssScene, camera);
}
}
function onResize() {
if (!renderer || !canvasRef.value) return;
const r = canvasRef.value.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
if (!renderer || !cssRenderer || !canvasRef.value) {
return;
}
const width = canvasRef.value.clientWidth;
const height = canvasRef.value.clientHeight;
const rect = canvasRef.value.getBoundingClientRect();
const width = rect.width || 1;
const height = rect.height || 1;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(width, height, false);
cssRenderer.setSize(width, height);
camera.aspect = (r.width || 1) / (r.height || 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
const resizeObserver = new ResizeObserver(onResize);
function createComputerScreen(mesh) {
const iframeWidth = 1280;
const iframeHeight = 720;
const container = document.createElement("div");
container.style.width = `${iframeWidth}px`;
container.style.height = `${iframeHeight}px`;
container.style.background = "#050807";
container.style.overflow = "hidden";
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 = "/computer";
iframe.title = "Computer";
iframe.tabIndex = 0;
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.border = "0";
iframe.style.pointerEvents = "none";
container.appendChild(iframe);
// Zusätzlicher Fallback am iframe-Element selbst.
iframe.addEventListener("mouseleave", () => {
if (computerMode) leaveComputerMode();
});
iframe.addEventListener("load", () => {
try {
iframe.contentWindow.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
leaveComputerMode();
}
});
} catch (error) {
console.warn("Kein Zugriff auf iframe-Inhalt", error);
}
});
iframeObject = new CSS3DObject(container);
iframeObject.name = "computer-iframe";
iframeObject.visible = false;
cssScene.add(iframeObject);
updateIframeTransform(mesh, iframeWidth, iframeHeight);
}
function updateIframeTransform(mesh, iframeWidth, iframeHeight) {
mesh.updateWorldMatrix(true, false);
const worldPosition = new THREE.Vector3();
const worldQuaternion = new THREE.Quaternion();
const worldScale = new THREE.Vector3();
mesh.getWorldPosition(worldPosition);
mesh.getWorldQuaternion(worldQuaternion);
mesh.getWorldScale(worldScale);
mesh.geometry.computeBoundingBox();
const meshSize = new THREE.Vector3();
mesh.geometry.boundingBox.getSize(meshSize);
iframeObject.position.copy(worldPosition);
iframeObject.quaternion.copy(worldQuaternion);
iframeObject.scale.set(
(meshSize.x * worldScale.x + 0.03) / iframeWidth,
(meshSize.y * worldScale.y + 0.03) / iframeHeight,
1,
);
// Etwas vor den eigentlichen Bildschirm setzen,
// damit es nicht genau auf derselben Ebene liegt.
const normal = new THREE.Vector3(0, 0, -1.4)
.applyQuaternion(worldQuaternion)
.normalize();
iframeObject.position.addScaledVector(normal, -0.2);
}
let resizeObserver;
onMounted(() => {
initThree(canvasRef.value);
onResize();
canvasRef.value.addEventListener("click", onCanvasClick);
const canvas = canvasRef.value;
const cssContainer = cssRendererRef.value;
if (!canvas || !cssContainer) {
console.error("Renderer-Elemente fehlen", {
canvas,
cssContainer,
});
return;
}
initThree(canvas, cssContainer);
// Erst nach initThree aufrufen
onResize();
canvas.addEventListener("click", onCanvasClick);
resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(canvas);
resizeObserver.observe(canvasRef.value);
window.addEventListener("resize", onResize);
window.addEventListener("message", onIframeMessage);
});
onUnmounted(() => {
gsap.ticker.remove(render);
window.removeEventListener("resize", onResize);
window.removeEventListener("message", onIframeMessage);
canvasRef.value?.removeEventListener("click", onCanvasClick);
resizeObserver.disconnect();
resizeObserver?.disconnect();
cameraAnimation?.kill();
controls?.dispose();
renderer?.dispose();
cssRenderer?.domElement?.remove();
gsapCtx?.revert();
// ScrollTrigger.getAll().forEach((t) => t.kill());
});
</script>
<template>
<div class="wrapper">
<!-- <div class="spacer"></div> -->
<canvas class="canvas" ref="canvasRef"></canvas>
</div>
<div class="scroll-buffer"></div>
<div class="spacer"></div>
<section class="pc-books" v-bind="$attrs">
<div class="wrapper">
<div ref="cssRendererRef" class="css-layer"></div>
<canvas ref="canvasRef" class="canvas"></canvas>
</div>
<!-- <div class="scroll-buffer"></div> -->
<div class="spacer"></div>
</section>
</template>
<style>
@@ -253,11 +495,38 @@ onUnmounted(() => {
height: 100vh;
}
.canvas {
width: 100%;
height: 100%;
}
.scroll-buffer {
height: 100vh;
}
.wrapper {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.css-layer {
position: absolute;
inset: 0;
z-index: 3;
pointer-events: none;
}
.css-layer .css-renderer {
pointer-events: none;
}
.canvas {
position: absolute;
inset: 0;
z-index: 2;
width: 100%;
height: 100%;
pointer-events: auto;
}
.css-layer iframe {
display: block;
pointer-events: none;
}
</style>

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>

View File

@@ -144,15 +144,15 @@ const emit = defineEmits<{
</script>
<style scoped>
:global(*) {
* {
box-sizing: border-box;
}
:global(html) {
html {
scroll-behavior: smooth;
}
:global(body) {
body {
margin: 0;
background: #f3eee3;
color: #29251f;

View File

@@ -5,13 +5,17 @@ import BooksShow from '../views/BooksShow.vue'
import SlimMain from '../views/HomeSlime.vue'
import BigMeImg from '../views/ImageSection.vue'
import OsWebseite from '../components/computer/OsWebseite.vue'
const routes: RouteRecordRaw[] = [
{ path: '/', component: SlimMain },
{ path: '/motion', component: HomeBig },
{path: '/motion/me', component: BigMeImg},
{path: '/books', component: BooksShow}
{path: '/books', component: BooksShow},
{
path: "/computer", component: OsWebseite},
// { path: '/ducks',
// component: DucksInfo,

View File

@@ -42,8 +42,8 @@ onUnmounted(() => {
<!-- <NewTextSection id="newText" /> -->
<IntroductionSection />
<MonitorModel id="model" />
<PcBooks id="model_pc" />
<MonitorModel id="model" />
<!-- <HeroSection id="hero" /> -->
<!-- <ImageSection id="image" /> -->
<TextSection id="text" />
@@ -51,4 +51,8 @@ onUnmounted(() => {
</div>
</template>
<style lang="css" scoped></style>
<style lang="css">
body {
background-color: black;
}
</style>

View File

@@ -130,7 +130,6 @@ onUnmounted(() => {
<template>
<div id="smooth-wrapper">
<div id="smooth-content">
in d
<Main id="welcome" @scroll-to-next="scrollTo" />
<section class="component-wrapper">
@@ -145,6 +144,9 @@ onUnmounted(() => {
</template>
<style scoped>
body {
background-color: black;
}
.component-wrapper {
position: relative;
width: 100%;