Compare commits

...

4 Commits

Author SHA1 Message Date
T-A-H-prog
1459fd3d2f styles & new model 2026-09-12 20:45:20 +02:00
T-A-H-prog
e33c509856 iframe 2026-09-10 21:07:05 +02:00
T-A-H-prog
6eaa10e34f new model 2026-09-10 18:25:28 +02:00
T-A-H-prog
109985a921 new components 2026-09-09 19:37:28 +02:00
18 changed files with 2037 additions and 20 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.

Binary file not shown.

Binary file not shown.

BIN
public/vids/th.mp4 Normal file

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

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

View File

@@ -0,0 +1,570 @@
<script setup lang="js">
import { onMounted, onUnmounted, ref } from "vue";
import * as THREE from "three";
import gsap from "gsap";
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";
defineOptions({ inheritAttrs: false });
const canvasRef = ref(null);
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 screenMesh;
let iframe;
let iframeObject;
let computerMode = false;
let cameraAnimation = null;
let iframeInteractionReady = false;
let iframePointerWasInside = false;
let iframeReadyTimer = null;
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(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);
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = false;
controls.enableDamping = true;
controls.maxPolarAngle = Math.PI / 2;
controls.minAzimuthAngle = -Math.PI / 2;
controls.maxAzimuthAngle = Math.PI / 2;
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.102));
loadModel();
gsap.ticker.add(render);
}
function loadModel() {
const modelUrl = "/models/book_shelf-v1.glb";
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/");
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);
loader.load(
modelUrl,
(gltf) => {
model = gltf.scene;
scene.add(model);
// Bounding Box nach dem Hinzufügen bestimmen
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
// Kamera passend zum Objekt positionieren
const distance = maxSize * 2;
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);
// camera.lookAt(center);
camera.position.set(
center.x,
center.y + distance * 0.35,
center.z + distance,
);
camera.lookAt(center);
controls.target.copy(center);
controls.update();
// OrbitControls auf das Objekt ausrichten
controls.target.copy(center);
controls.update();
const bulb = model.getObjectByName("light");
if (bulb) {
console.log("Glühbirne gefunden:", bulb);
const pointLight = new THREE.PointLight(
0xffd6a3, // Farbe
5, // Intensität
10, // Reichweite
);
bulb.add(pointLight);
}
},
(progress) => {
if (progress.total) {
const percent = ((progress.loaded / progress.total) * 100).toFixed(1);
document.title = percent;
console.log(`Loading: ${percent}%`);
}
},
(error) => {
console.error("Fehler beim Laden des Modells:", error);
document.title = "Fehler beim Laden!";
},
);
}
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 intersects = raycaster.intersectObject(screenMesh, true);
if (intersects.length > 0) {
enterComputerMode();
}
}
function enterComputerMode() {
if (!screenMesh || computerMode) return;
computerMode = true;
iframeInteractionReady = false;
iframePointerWasInside = false;
window.clearTimeout(iframeReadyTimer);
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, 2, 0);
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("input")?.focus();
} catch (error) {
console.warn("iframe konnte nicht fokussiert werden", error);
}
// Verhindert, dass ein Lade-/Layout-event sofort wieder herauszoomt.
iframeReadyTimer = window.setTimeout(() => {
iframeInteractionReady = true;
}, 300);
},
});
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;
iframeInteractionReady = false;
iframePointerWasInside = false;
window.clearTimeout(iframeReadyTimer);
iframe.style.pointerEvents = "none";
iframe.blur();
// Noch sichtbar lassen während der Kamera-Animation
iframeObject.visible = true;
screenMesh.visible = true;
cameraAnimation?.kill();
cameraAnimation = gsap.timeline({
onUpdate() {
camera.lookAt(controls.target);
},
onComplete() {
controls.enabled = true;
controls.update();
// Erst nach dem Herauszoomen ausblenden
iframeObject.visible = false;
},
});
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") {
if (!computerMode || !iframeInteractionReady) return;
leaveComputerMode();
}
}
function render() {
if (!renderer || !scene || !camera) return;
if (!computerMode) {
controls?.update();
}
renderer.render(scene, camera);
if (cssRenderer && cssScene) {
cssRenderer.render(cssScene, camera);
}
}
function onResize() {
if (!renderer || !cssRenderer || !canvasRef.value) {
return;
}
const rect = canvasRef.value.getBoundingClientRect();
const width = rect.width || 1;
const height = rect.height || 1;
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(width, height, false);
cssRenderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function createComputerScreen(mesh) {
const iframeWidth = 1480;
const iframeHeight = 920;
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");
iframe.src = `${window.location.origin}/motion/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);
// Erst ein echtes Betreten und anschließendes Verlassen schließt den Modus.
iframe.addEventListener("pointerenter", () => {
if (!computerMode) return;
iframePointerWasInside = true;
});
iframe.addEventListener("pointerleave", () => {
if (!computerMode || !iframeInteractionReady || !iframePointerWasInside) {
return;
}
leaveComputerMode();
});
iframe.addEventListener("load", () => {
try {
iframe.contentWindow.addEventListener("keydown", (event) => {
if (event.key === "Escape" && computerMode && iframeInteractionReady) {
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);
/*
* 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(
screenWidth / iframeWidth,
screenHeight / iframeHeight,
1,
);
/*
* Vorderseite nach der Korrekturrotation berechnen.
*/
const iframeNormal = new THREE.Vector3(0, 0, 1)
.applyQuaternion(iframeObject.quaternion)
.normalize();
iframeObject.position.addScaledVector(iframeNormal, 0.1);
}
let resizeObserver;
onMounted(() => {
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);
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();
cameraAnimation?.kill();
window.clearTimeout(iframeReadyTimer);
controls?.dispose();
renderer?.dispose();
cssRenderer?.domElement?.remove();
gsapCtx?.revert();
});
</script>
<template>
<div class="scroll-buffer"></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>
.spacer {
width: 100%;
height: 200vh;
display: grid;
place-items: center;
font-weight: 600;
}
.wrapper {
width: 100%;
height: 100vh;
}
.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,990 @@
<script setup lang="js">
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
const code = ref("");
const loggedIn = ref(false);
const error = ref("");
const inputRef = ref(null);
const desktopRef = ref(null);
const caseWindowRef = ref(null);
const caseOpen = ref(false);
const currentTime = ref("");
const windowPosition = reactive({ x: 90, y: 48 });
const drag = { active: false, offsetX: 0, offsetY: 0 };
let clockTimer;
const caseWindowStyle = computed(() => ({
transform: `translate3d(${windowPosition.x}px, ${windowPosition.y}px, 0)`,
}));
function cleanCode(event) {
code.value = event.target.value.replace(/\D/g, "").slice(0, 4);
error.value = "";
}
function login() {
if (code.value !== "1337") {
code.value = "";
error.value = "ACCESS DENIED";
nextTick(() => inputRef.value?.focus());
return;
}
loggedIn.value = true;
error.value = "";
}
function logout() {
caseOpen.value = false;
loggedIn.value = false;
code.value = "";
error.value = "";
nextTick(() => inputRef.value?.focus());
}
function openCase() {
caseOpen.value = true;
nextTick(() => {
const desktop = desktopRef.value;
const caseWindow = caseWindowRef.value;
if (!desktop || !caseWindow) return;
windowPosition.x = Math.max(
8,
Math.round((desktop.clientWidth - caseWindow.offsetWidth) / 2),
);
windowPosition.y = Math.max(
8,
Math.round((desktop.clientHeight - caseWindow.offsetHeight) / 2),
);
caseWindow.focus();
});
}
function closeCase() {
caseOpen.value = false;
drag.active = false;
}
function startDrag(event) {
if (event.button !== 0) return;
const caseWindow = caseWindowRef.value;
if (!caseWindow) return;
drag.active = true;
drag.offsetX = event.clientX - caseWindow.getBoundingClientRect().left;
drag.offsetY = event.clientY - caseWindow.getBoundingClientRect().top;
event.currentTarget.setPointerCapture?.(event.pointerId);
event.preventDefault();
}
function moveDrag(event) {
if (!drag.active) return;
const desktop = desktopRef.value;
const caseWindow = caseWindowRef.value;
if (!desktop || !caseWindow) return;
const desktopRect = desktop.getBoundingClientRect();
const maxX = Math.max(0, desktop.clientWidth - caseWindow.offsetWidth);
const maxY = Math.max(0, desktop.clientHeight - caseWindow.offsetHeight);
windowPosition.x = Math.min(
maxX,
Math.max(0, event.clientX - desktopRect.left - drag.offsetX),
);
windowPosition.y = Math.min(
maxY,
Math.max(0, event.clientY - desktopRect.top - drag.offsetY),
);
}
function stopDrag() {
drag.active = false;
}
function updateClock() {
currentTime.value = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}).format(new Date());
}
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(() => {
updateClock();
clockTimer = window.setInterval(updateClock, 1000);
window.addEventListener("keydown", onKeydown);
window.addEventListener("pointermove", moveDrag);
window.addEventListener("pointerup", stopDrag);
window.addEventListener("pointercancel", stopDrag);
inputRef.value?.focus();
});
onUnmounted(() => {
window.clearInterval(clockTimer);
window.removeEventListener("keydown", onKeydown);
window.removeEventListener("pointermove", moveDrag);
window.removeEventListener("pointerup", stopDrag);
window.removeEventListener("pointercancel", stopDrag);
});
</script>
<template>
<main class="page">
<section v-if="!loggedIn" class="login-screen">
<div class="login-box">
<div class="login-brand">
<span class="seal">POLICE</span>
<div>
<b>FEDERAL BUREAU OF INVESTIGATION</b>
<small>INTERNAL RECORDS NETWORK</small>
</div>
</div>
<h1>Restricted System</h1>
<p>Enter your assigned access code.</p>
<form @submit.prevent="login">
<label for="access-code">ACCESS CODE</label>
<input
id="access-code"
ref="inputRef"
:value="code"
type="password"
inputmode="numeric"
maxlength="4"
autocomplete="off"
@input="cleanCode"
/>
<button type="submit">LOG IN</button>
</form>
<p v-if="error" class="error" role="alert">{{ error }}</p>
<small class="system-note"
>Fictional archive system // ESC to exit</small
>
</div>
</section>
<section v-else class="desktop-frame">
<div ref="desktopRef" class="desktop">
<header class="desktop-header">
<div class="wordmark">
<span class="mini-seal">FBI</span>
<div>
<b>FBI CASE MANAGEMENT</b>
<small>WASHINGTON SECURE NODE</small>
</div>
</div>
<span class="classification">UNCLASSIFIED // TRAINING SYSTEM</span>
</header>
<div class="desktop-files" aria-label="Desktop files">
<button class="file-icon" type="button" @click="openCase">
<span class="folder" aria-hidden="true"><i>TH</i></span>
<span>TH_CASE</span>
</button>
<button class="file-icon muted" type="button">
<span class="document-icon" aria-hidden="true"></span>
<span>README.TXT</span>
</button>
<button class="file-icon muted" type="button">
<span class="folder" aria-hidden="true"><i>DB</i></span>
<span>ARCHIVE</span>
</button>
</div>
<article
v-if="caseOpen"
ref="caseWindowRef"
class="case-window"
:style="caseWindowStyle"
tabindex="-1"
>
<div class="window-titlebar" @pointerdown="startDrag">
<span>TH_CASE — CASE FILE VIEWER</span>
<button
type="button"
aria-label="Close case file"
@pointerdown.stop
@click="closeCase"
>
×
</button>
</div>
<div class="window-menu">
File&nbsp;&nbsp; Edit&nbsp;&nbsp; View&nbsp;&nbsp; Security
</div>
<div class="case-scroll">
<section class="paper-file">
<div class="paper-tab">TH / 04-1337</div>
<div class="case-masthead">
<div class="case-seal">FBI</div>
<div>
<small>UNITED STATES DEPARTMENT OF JUSTICE</small>
<h1>CASE INVESTIGATION RECORD</h1>
<p>FEDERAL BUREAU OF INVESTIGATION</p>
</div>
<div class="stamp">CONFIDENTIAL</div>
</div>
<dl class="case-fields">
<div>
<dt>FILE NUMBER</dt>
<dd>TH-04-1337</dd>
</div>
<div>
<dt>STATUS</dt>
<dd>ACTIVE</dd>
</div>
<div>
<dt>SUBJECT</dt>
<dd>HERPEL, TOM</dd>
</div>
<div>
<dt>ALIAS</dt>
<dd>"TH"</dd>
</div>
<div>
<dt>DATE OPENED</dt>
<dd>09 / 12 / 2001</dd>
</div>
<div>
<dt>ASSIGNED UNIT</dt>
<dd>DIGITAL RECORDS DIVISION</dd>
</div>
</dl>
<section class="case-section">
<h2>SUBJECT SUMMARY</h2>
<p>
Subject is connected to a series of experimental computer
systems and unidentified digital transmissions. Activity has
been observed across several independent network locations.
</p>
</section>
<section class="case-section">
<h2>KNOWN INFORMATION</h2>
<table>
<tbody>
<tr>
<th>Occupation</th>
<td>Software Developer</td>
</tr>
<tr>
<th>Primary skill</th>
<td>Interactive systems</td>
</tr>
<tr>
<th>Last known area</th>
<td>Central Europe</td>
</tr>
<tr>
<th>Risk assessment</th>
<td>Low / observation only</td>
</tr>
</tbody>
</table>
</section>
<section class="case-section">
<h2>ACTIVITY LOG</h2>
<ol class="activity-log">
<li>
<time>09/12/01</time
><span>Case file created by Records Division.</span>
</li>
<li>
<time>09/18/01</time
><span>Unidentified terminal activity recorded.</span>
</li>
<li>
<time>10/02/01</time
><span>Archive reference TH confirmed.</span>
</li>
<li>
<time>10/11/01</time
><span>Investigation status retained as active.</span>
</li>
</ol>
</section>
<section class="case-section last-section">
<h2>AGENT NOTE</h2>
<p>
Maintain passive monitoring. No contact has been authorized.
Refer all additional material to file TH-04-1337.
</p>
<div class="signature">SIGNED // SPECIAL AGENT R. KELLER</div>
</section>
</section>
</div>
</article>
<footer class="taskbar">
<button class="start-button" type="button"><b>FBI</b> Start</button>
<button
v-if="caseOpen"
class="task-button"
type="button"
@click="openCase"
>
TH_CASE
</button>
<span class="task-spacer"></span>
<button class="logout-button" type="button" @click="logout">
Log off
</button>
<time>{{ currentTime }}</time>
</footer>
</div>
</section>
</main>
</template>
<style scoped>
* {
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #000;
}
body {
font-family: Tahoma, Verdana, Arial, sans-serif;
cursor: default !important;
}
button,
input {
font: inherit;
}
.page {
width: 100%;
height: 100dvh;
padding: clamp(14px, 2.5vmin, 30px);
overflow: hidden;
color: #d7ddd9;
background: #000;
font-size: clamp(12px, 1.3vw, 16px);
}
.login-screen,
.desktop-frame {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
.login-screen {
display: grid;
place-items: center;
}
.login-box {
width: min(440px, calc(100% - 34px));
padding: 24px;
border: 1px solid #4e5c54;
background: #000;
}
.login-brand,
.wordmark {
display: flex;
align-items: center;
gap: 12px;
}
.seal,
.mini-seal,
.case-seal {
display: grid;
place-items: center;
border: 2px solid currentColor;
border-radius: 50%;
font-family: Georgia, serif;
font-weight: 700;
}
.seal {
width: 56px;
height: 56px;
color: #9daaa2;
}
.login-brand b,
.login-brand small,
.wordmark b,
.wordmark small {
display: block;
}
.login-brand b {
font:
700 0.82em "Courier New",
monospace;
}
.login-brand small,
.wordmark small {
margin-top: 3px;
color: #68776e;
font-size: 0.64em;
}
.login-box h1 {
margin: 28px 0 8px;
font:
700 1.35em "Courier New",
monospace;
}
.login-box p {
margin: 0;
color: #89958e;
}
.login-box form {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
margin-top: 24px;
}
.login-box label {
grid-column: 1 / -1;
color: #78867d;
font:
0.72em "Courier New",
monospace;
}
.login-box input,
.login-box button {
height: 34px;
border: 1px solid #526158;
border-radius: 0;
}
.login-box input {
min-width: 0;
padding: 6px 9px;
outline: none;
color: #a9e7b9;
background: #000;
}
.login-box input:focus {
border-color: #8cad96;
}
.login-box button,
.taskbar button,
.window-titlebar button {
color: #d6ddd8;
background: #252d28;
cursor: pointer;
}
.login-box button {
padding: 0 16px;
}
.error {
margin-top: 14px !important;
color: #cf655e !important;
font-family: "Courier New", monospace;
}
.system-note {
display: block;
margin-top: 24px;
color: #536058;
}
.desktop-frame {
position: relative;
padding: 7px;
background: #000;
}
.desktop {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border: 1px solid #506057;
background:
linear-gradient(rgba(5, 18, 12, 0.94), rgba(5, 18, 12, 0.94)),
repeating-linear-gradient(
90deg,
transparent 0 39px,
rgba(125, 157, 137, 0.03) 40px
),
#07110c;
}
.desktop-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 58px;
padding: 0 18px;
border-bottom: 1px solid #3e5046;
background: #0b120e;
}
.mini-seal {
width: 36px;
height: 36px;
color: #9eafa5;
font-size: 0.74em;
}
.wordmark b {
color: #ced8d1;
font:
700 0.76em "Courier New",
monospace;
letter-spacing: 0.04em;
}
.classification {
color: #65756b;
font:
0.64em "Courier New",
monospace;
}
.desktop-files {
position: absolute;
top: 76px;
left: 16px;
display: grid;
grid-template-columns: repeat(2, 82px);
gap: 22px 12px;
}
.file-icon {
display: grid;
justify-items: center;
gap: 7px;
width: 82px;
padding: 4px;
border: 1px solid transparent;
color: #d9e0db;
background: transparent;
font-size: 0.68em;
cursor: pointer;
}
.file-icon:hover,
.file-icon:focus-visible {
border-color: #687c70;
outline: none;
background: rgba(118, 148, 130, 0.15);
}
.file-icon.muted {
color: #87928b;
}
.folder {
position: relative;
display: grid;
width: 48px;
height: 36px;
place-items: center;
border: 1px solid #8d8260;
background: #918256;
box-shadow: inset 0 0 0 2px #b0a170;
}
.folder::before {
content: "";
position: absolute;
bottom: 100%;
left: 3px;
width: 20px;
height: 6px;
border: 1px solid #8d8260;
border-bottom: 0;
background: #918256;
}
.folder i {
color: #191a16;
font:
700 0.72em Arial,
sans-serif;
font-style: normal;
}
.document-icon {
position: relative;
width: 34px;
height: 43px;
border: 1px solid #87948c;
background: #c8d0ca;
}
.document-icon::after {
content: "";
position: absolute;
top: 10px;
right: 6px;
bottom: 8px;
left: 6px;
background: repeating-linear-gradient(#6b746e 0 1px, transparent 1px 6px);
}
.case-window {
position: absolute;
top: 0;
left: 0;
z-index: 20;
display: grid;
grid-template-rows: 31px 25px minmax(0, 1fr);
width: min(76%, 100%);
height: min(78%, 90%);
min-width: 380px;
min-height: 260px;
overflow: hidden;
border: 2px solid #b5b9b6;
outline: 1px solid #181b19;
color: #131713;
background: #b9bdb9;
box-shadow: 7px 9px 0 rgba(0, 0, 0, 0.45);
}
.window-titlebar {
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 9px;
color: #fff;
background: #173e2a;
font:
700 0.72em Tahoma,
sans-serif;
cursor: move;
touch-action: none;
user-select: none;
}
.window-titlebar button {
width: 26px;
height: 23px;
margin-right: 3px;
padding: 0;
border: 1px outset #d0d4d1;
color: #111;
background: #c4c8c5;
font-size: 1.2em;
line-height: 1;
}
.window-menu {
padding: 5px 9px;
border-bottom: 1px solid #7f8580;
color: #222;
background: #c4c8c5;
font-size: 0.66em;
}
.case-scroll {
min-height: 0;
padding: 16px;
overflow-y: auto;
overscroll-behavior: contain;
background: #777d78;
scrollbar-color: #687169 #c2c5c2;
}
.paper-file {
position: relative;
width: min(100%, 792px);
min-height: 780px;
margin: 12px auto 28px;
padding: 42px clamp(24px, 5vw, 56px) 54px;
color: #25231e;
background: #ded9c8;
box-shadow: 3px 4px 0 rgba(0, 0, 0, 0.26);
font-family: "Courier New", monospace;
}
.paper-tab {
position: absolute;
bottom: 100%;
left: 18px;
padding: 6px 18px 5px;
border-bottom: 1px solid #afa992;
background: #cfc8af;
font-size: 0.72em;
font-weight: 700;
}
.case-masthead {
display: grid;
grid-template-columns: 62px 1fr auto;
align-items: center;
gap: 14px;
padding-bottom: 18px;
border-bottom: 3px double #4e4a3e;
text-align: center;
}
.case-seal {
width: 58px;
height: 58px;
color: #343129;
font-size: 1.15em;
}
.case-masthead small,
.case-masthead p {
font-size: 0.62em;
}
.case-masthead h1 {
margin: 4px 0;
font-size: 1.22em;
line-height: 1.1;
}
.case-masthead p {
margin: 0;
}
.stamp {
padding: 7px 8px;
border: 3px double #8e302b;
color: #8e302b;
font-size: 0.64em;
font-weight: 700;
transform: rotate(-5deg);
}
.case-fields {
display: grid;
grid-template-columns: 1fr 1fr;
margin: 24px 0;
border-top: 1px solid #777264;
border-left: 1px solid #777264;
}
.case-fields div {
padding: 8px 10px;
border-right: 1px solid #777264;
border-bottom: 1px solid #777264;
}
.case-fields dt {
font-size: 0.9em;
font-weight: 900;
}
.case-fields dd {
margin: 4px 0 0;
font-size: 0.78em;
}
.case-section {
margin-top: 26px;
}
.case-section h2 {
margin: 0 0 9px;
padding-bottom: 4px;
border-bottom: 1px solid #777264;
font-size: 0.8em;
letter-spacing: 0.08em;
}
.case-section p {
margin: 0;
font-size: 0.77em;
line-height: 1.65;
}
.case-section table {
width: 100%;
border-collapse: collapse;
font-size: 0.72em;
}
.case-section th,
.case-section td {
padding: 7px 9px;
border: 1px solid #888273;
text-align: left;
}
.case-section th {
width: 34%;
background: rgba(75, 69, 56, 0.08);
}
.activity-log {
margin: 0;
padding: 0;
list-style: none;
font-size: 0.73em;
}
.activity-log li {
display: grid;
grid-template-columns: 80px 1fr;
gap: 12px;
padding: 7px 0;
border-bottom: 1px dotted #948e7d;
}
.activity-log time {
font-weight: 700;
}
.last-section {
margin-bottom: 22px;
}
.signature {
margin-top: 26px;
font-size: 0.68em;
text-align: right;
}
.taskbar {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 40;
display: flex;
align-items: center;
gap: 5px;
height: 34px;
padding: 3px 5px;
border-top: 2px outset #a6ada8;
color: #171a18;
background: #aeb4b0;
}
.taskbar button {
height: 27px;
padding: 0 10px;
border: 2px outset #d1d5d2;
color: #121512;
background: #b9bebb;
font-size: 0.7em;
}
.start-button {
min-width: 72px;
}
.start-button b {
color: #173e2a;
}
.task-button {
width: min(180px, 28vw);
text-align: left;
}
.task-spacer {
flex: 1;
}
.taskbar time {
min-width: 76px;
padding: 5px 7px;
border: 1px inset #d1d5d2;
font-size: 0.68em;
text-align: center;
}
@media (max-width: 700px), (max-height: 540px) {
.page {
padding: 10px;
}
.desktop-header {
height: 46px;
}
.classification,
.wordmark small {
display: none;
}
.desktop-files {
top: 62px;
}
.case-window {
width: calc(100% - 16px);
height: calc(100% - 50px);
min-width: 0;
min-height: 0;
}
.paper-file {
padding-right: 22px;
padding-left: 22px;
}
}
@media (max-width: 500px) {
.case-masthead {
grid-template-columns: 48px 1fr;
}
.case-seal {
width: 44px;
height: 44px;
}
.stamp {
display: none;
}
.case-fields {
grid-template-columns: 1fr;
}
}
</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

@@ -0,0 +1,11 @@
<template>
<h2 @click="emit('morphTo', '#me')">Books</h2>
</template>
<script setup lang="ts">
const emit = defineEmits<{
morphTo: [target: string];
}>();
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,11 @@
<template>
<h2 @click="emit('morphTo', '#me')">Coding</h2>
</template>
<script setup lang="ts">
const emit = defineEmits<{
morphTo: [target: string];
}>();
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<h2 @click="emit('morphTo', '#me')">Hallo</h2>
<h2 @click="emit('morphTo', '#me')">Goals</h2>
</template>
<script setup lang="ts">

View File

@@ -0,0 +1,11 @@
<template>
<h2 @click="emit('morphTo', '#me')">Videos</h2>
</template>
<script setup lang="ts">
const emit = defineEmits<{
morphTo: [target: string];
}>();
</script>
<style lang="scss" scoped></style>

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: "/motion/computer", component: OsWebseite},
// { path: '/ducks',
// component: DucksInfo,

View File

@@ -9,6 +9,7 @@ import ScrollTrigger from "gsap/ScrollTrigger";
import ScrollSmoother from "gsap/ScrollSmoother";
import MonitorModel from "../components/big/MonitorModel.vue";
import IntroductionSection from "../components/big/IntroductionSection.vue";
import PcBooks from "../components/big/PcBooks.vue";
// import NewTextSection from "../components/sections/NewTextSection.vue";
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
@@ -41,6 +42,7 @@ onUnmounted(() => {
<!-- <NewTextSection id="newText" /> -->
<IntroductionSection />
<PcBooks id="model_pc" />
<MonitorModel id="model" />
<!-- <HeroSection id="hero" /> -->
<!-- <ImageSection id="image" /> -->
@@ -49,4 +51,8 @@ onUnmounted(() => {
</div>
</template>
<style lang="css" scoped></style>
<style lang="css">
body {
background-color: black;
}
</style>

View File

@@ -1,14 +1,16 @@
<script setup lang="ts">
import Main from "../components/slim/Main.vue";
import AboutMe from "../components/slim/AboutMe.vue";
import Goals from "../components/slim/Goals.vue";
import Goals from "../components/slim/interressen/Goals.vue";
import Coding from "../components/slim/interressen/Coding.vue";
import Books from "../components/slim/interressen/Books.vue";
import Videos from "../components/slim/interressen/Videos.vue";
import { onMounted, onUnmounted, nextTick, ref } from "vue";
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
import ScrollSmoother from "gsap/ScrollSmoother";
import { S } from "vue-router/dist/router-CWoNjPRp.mjs";
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
@@ -34,35 +36,65 @@ const scrollTo = (target: string) => {
smoother.scrollTo(target, true);
};
const currentSection = ref("#me");
const morphTo = (target: string) => {
if (transitioning.value) return;
const current =
target === "#goals"
? document.querySelector("#me")
: document.querySelector("#goals");
// Bereits auf dieser Section? Nichts tun.
if (currentSection.value === target) return;
const current = document.querySelector(currentSection.value);
const next = document.querySelector(target);
if (!current || !next) return;
transitioning.value = true;
// Ziel vorbereiten
gsap.set(next, {
opacity: 0,
scale: 0.85,
filter: "blur(20px)",
clipPath: "circle(0% at 50% 50%)",
pointerEvents: "none",
zIndex: 2,
});
// Aktuelles Element nach vorne
gsap.set(current, {
zIndex: 1,
transformOrigin: "50% 50%",
pointerEvents: "auto",
});
const tl = gsap.timeline({
onComplete: () => {
// Aktuellen Zustand speichern
currentSection.value = target;
// Altes Element komplett deaktivieren
gsap.set(current, {
opacity: 0,
pointerEvents: "none",
scale: 1,
filter: "blur(0px)",
clipPath: "none",
zIndex: 1,
});
// Neues Element vollständig aktivieren
gsap.set(next, {
opacity: 1,
pointerEvents: "auto",
scale: 1,
filter: "blur(0px)",
clipPath: "none",
zIndex: 2,
});
transitioning.value = false;
scrollTo(target);
},
});
@@ -74,12 +106,10 @@ const morphTo = (target: string) => {
duration: 0.4,
ease: "power1.in",
})
.set(next, {
opacity: 1,
pointerEvents: "auto",
})
.to(next, {
scale: 1,
filter: "blur(0px)",
@@ -104,25 +134,36 @@ onUnmounted(() => {
<section class="component-wrapper">
<AboutMe id="me" @morph-to="morphTo" />
<Goals id="goals" @morph-to="morphTo" />
<Videos id="videos" @morph-to="morphTo" />
<Coding id="coding" @morph-to="morphTo" />
<Books id="books" @morph-to="morphTo" />
</section>
</div>
</div>
</template>
<style scoped>
body {
background-color: black;
}
.component-wrapper {
position: relative;
width: 100%;
}
#me,
#goals {
#goals,
#videos,
#coding,
#books {
width: 100%;
}
#goals {
#goals,
#videos,
#coding,
#books {
position: absolute;
inset: 0;