Compare commits

..

6 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
T-A-H-prog
78e35694bd morph 2026-08-13 22:14:16 +02:00
T-A-H-prog
16d4bf1e4b number 2026-08-13 20:13:36 +02:00
21 changed files with 2261 additions and 47 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 { body {
font-family: Inter, system-ui, sans-serif; font-family: Inter, system-ui, sans-serif;
background: #0f0f0f;
color: white; color: white;
overflow-x: hidden; overflow-x: hidden;
cursor: cursor:

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -106,7 +106,7 @@ onUnmounted(() => {
<p> <p>
Nach Schelling ist die Kunst das Erzeugnis oder die Folge jener Nach Schelling ist die Kunst das Erzeugnis oder die Folge jener
Weltanschauung, nach der das Subjekt sich in sein Objekt verwandelt oder Weltanschauung, nach der das Subjekt sich in sein Objekt verwandelt oder
das Objekt selbst 45 zum Subjekt wird. Die Schönheit ist die Darstellung das Objekt selbst zum Subjekt wird. Die Schönheit ist die Darstellung
des Unendli- chen im Endlichen. Und der Grundcharakter eines Kunstwerkes des Unendli- chen im Endlichen. Und der Grundcharakter eines Kunstwerkes
ist die bewußtlose Unendlichkeit. Die Kunst ist die Vereinigung des ist die bewußtlose Unendlichkeit. Die Kunst ist die Vereinigung des
Subjektiven mit dem Objektiven, – der Natur mit der Vernunft, der Subjektiven mit dem Objektiven, – der Natur mit der Vernunft, der

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

@@ -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

@@ -74,13 +74,26 @@
<span class="number">03 / 04</span> <span class="number">03 / 04</span>
</div> </div>
<div> <div class="interests">
<span class="label">Based in</span> <span class="label">Misc</span>
<p class="small location"> <ul class="interessen-list">
Germany<br /> <li
Europe v-for="(interest, index) in interests"
</p> :key="interest.name"
@click="emit('morphTo', interest.target)"
>
<span class="number">
{{ String(index + 1).padStart(2, "0") }}
</span>
<span class="interest-name">
{{ interest.name }}
</span>
<span class="arrow">↗</span>
</li>
</ul>
</div> </div>
</section> </section>
@@ -99,20 +112,47 @@
</main> </main>
</template> </template>
<script setup> <script setup lang="ts">
import tom_img from "../../assets/myself/SmartTom.jpeg"; import tom_img from "../../assets/myself/SmartTom.jpeg";
const interests = [
{
name: "Goals",
target: "#goals",
},
{
name: "Coding",
target: "#coding",
},
{
name: "Videos",
target: "#videos",
},
{
name: "Books",
target: "#books",
},
{
name: "Experiences",
target: "#experiences",
},
];
const emit = defineEmits<{
morphTo: [target: string];
}>();
</script> </script>
<style scoped> <style scoped>
:global(*) { * {
box-sizing: border-box; box-sizing: border-box;
} }
:global(html) { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
:global(body) { body {
margin: 0; margin: 0;
background: #f3eee3; background: #f3eee3;
color: #29251f; color: #29251f;
@@ -317,10 +357,16 @@ import tom_img from "../../assets/myself/SmartTom.jpeg";
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
.interests {
width: 100%;
}
.label { .label {
font-size: 11px; display: block;
font-weight: 700; margin-bottom: 30px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
@@ -357,16 +403,63 @@ import tom_img from "../../assets/myself/SmartTom.jpeg";
font-weight: 700; font-weight: 700;
} }
.skill-list li { .skill-list li,
.interressen-list li {
padding: 6px 0; padding: 6px 0;
border-top: 1px solid rgba(41, 37, 31, 0.35); border-top: 1px solid rgba(41, 37, 31, 0.35);
} }
/*
.skill-list li:last-child { .skill-list li:last-child,
.interressen-list li:last-child {
border-bottom: 1px solid rgba(41, 37, 31, 0.35); border-bottom: 1px solid rgba(41, 37, 31, 0.35);
} */
.interessen-list li {
display: grid;
grid-template-columns: 50px 1fr 30px;
align-items: center;
min-height: 70px;
border-top: 1px solid rgba(31, 31, 31, 0.35);
cursor: pointer;
transition:
padding-left 0.4s cubic-bezier(0.22, 1, 0.36, 1),
background-color 0.3s ease;
} }
.interessen-list li:last-child {
border-bottom: none;
}
.number {
font-size: 12px;
color: #777;
}
.interest-name {
font-size: clamp(24px, 3vw, 42px);
font-weight: 500;
line-height: 1;
letter-spacing: -0.04em;
}
.arrow {
font-size: 24px;
justify-self: end;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.interessen-list li:hover {
padding-left: 20px;
}
.interessen-list li:hover .arrow {
transform: translateX(8px) rotate(45deg);
}
.footer-links { .footer-links {
display: flex; display: flex;
gap: 25px; gap: 25px;

View File

@@ -17,9 +17,9 @@
</div> </div>
</section> </section>
<button class="div14 nav-item"> <button class="div14 nav-item" @click="emit('scrollToNext', '#me')">
<span>01</span> <span>01</span>
<span>About</span> <span>About Me</span>
</button> </button>
<button class="div15 nav-item"> <button class="div15 nav-item">
@@ -82,7 +82,7 @@
</div> </div>
</section> </section>
<section class="div21 small-block"> <section class="div21 small-block" @click="emit('scrollToNext', '#me')">
<span>03</span> <span>03</span>
<span>Scroll</span> <span>Scroll</span>
<span class="arrow">↓</span> <span class="arrow">↓</span>
@@ -106,10 +106,13 @@
</main> </main>
</template> </template>
<script setup> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
const emit = defineEmits<{
scrollToNext: [target: string];
}>();
const isInteractiveHovered = ref(true); const isInteractiveHovered = ref(false);
</script> </script>
<style scoped> <style scoped>
@@ -201,7 +204,7 @@ const isInteractiveHovered = ref(true);
} }
.div1 { .div1 {
grid-column: span 5 / span 5; grid-column: span 4 / span 4;
grid-row: span 2 / span 2; grid-row: span 2 / span 2;

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

@@ -0,0 +1,11 @@
<template>
<h2 @click="emit('morphTo', '#me')">Goals</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')">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 SlimMain from '../views/HomeSlime.vue'
import BigMeImg from '../views/ImageSection.vue' import BigMeImg from '../views/ImageSection.vue'
import OsWebseite from '../components/computer/OsWebseite.vue'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ path: '/', component: SlimMain }, { path: '/', component: SlimMain },
{ path: '/motion', component: HomeBig }, { path: '/motion', component: HomeBig },
{path: '/motion/me', component: BigMeImg}, {path: '/motion/me', component: BigMeImg},
{path: '/books', component: BooksShow} {path: '/books', component: BooksShow},
{
path: "/motion/computer", component: OsWebseite},
// { path: '/ducks', // { path: '/ducks',
// component: DucksInfo, // component: DucksInfo,

View File

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

View File

@@ -1,8 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import Main from "../components/slim/Main.vue"; import Main from "../components/slim/Main.vue";
import AboutMe from "../components/slim/AboutMe.vue"; import AboutMe from "../components/slim/AboutMe.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 { onMounted, onUnmounted, nextTick } from "vue";
import gsap from "gsap"; import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger"; import ScrollTrigger from "gsap/ScrollTrigger";
import ScrollSmoother from "gsap/ScrollSmoother"; import ScrollSmoother from "gsap/ScrollSmoother";
@@ -11,32 +16,158 @@ gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
let smoother: ScrollSmoother | null = null; let smoother: ScrollSmoother | null = null;
const transitioning = ref(false);
onMounted(async () => { onMounted(async () => {
await nextTick(); await nextTick();
if (smoother) return;
smoother = ScrollSmoother.create({ smoother = ScrollSmoother.create({
wrapper: "#smooth-wrapper", wrapper: "#smooth-wrapper",
content: "#smooth-content", content: "#smooth-content",
smooth: 1.5, smooth: 1.8,
effects: true, effects: true,
}); });
ScrollTrigger.refresh();
}); });
const scrollTo = (target: string) => {
if (!smoother) return;
smoother.scrollTo(target, true);
};
const currentSection = ref("#me");
const morphTo = (target: string) => {
if (transitioning.value) return;
// 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);
},
});
tl.to(current, {
scale: 0.92,
opacity: 0,
filter: "blur(15px)",
duration: 0.4,
ease: "power1.in",
})
.set(next, {
opacity: 1,
pointerEvents: "auto",
})
.to(next, {
scale: 1,
filter: "blur(0px)",
clipPath: "circle(150% at 50% 50%)",
duration: 0.8,
ease: "power4.inOut",
});
};
onUnmounted(() => { onUnmounted(() => {
smoother?.kill(); if (smoother) {
smoother = null; smoother.kill();
smoother = null;
}
}); });
</script> </script>
<template> <template>
<div id="smooth-wrapper"> <div id="smooth-wrapper">
<div id="smooth-content"> <div id="smooth-content">
<Main id="welcome" /> <Main id="welcome" @scroll-to-next="scrollTo" />
<AboutMe id="me" />
<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>
</div> </div>
</template> </template>
<style lang="css" scoped></style> <style scoped>
body {
background-color: black;
}
.component-wrapper {
position: relative;
width: 100%;
}
#me,
#goals,
#videos,
#coding,
#books {
width: 100%;
}
#goals,
#videos,
#coding,
#books {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
</style>

View File

@@ -106,7 +106,7 @@ onUnmounted(() => {
<p> <p>
Nach Schelling ist die Kunst das Erzeugnis oder die Folge jener Nach Schelling ist die Kunst das Erzeugnis oder die Folge jener
Weltanschauung, nach der das Subjekt sich in sein Objekt verwandelt oder Weltanschauung, nach der das Subjekt sich in sein Objekt verwandelt oder
das Objekt selbst 45 zum Subjekt wird. Die Schönheit ist die Darstellung das Objekt selbst zum Subjekt wird. Die Schönheit ist die Darstellung
des Unendli- chen im Endlichen. Und der Grundcharakter eines Kunstwerkes des Unendli- chen im Endlichen. Und der Grundcharakter eines Kunstwerkes
ist die bewußtlose Unendlichkeit. Die Kunst ist die Vereinigung des ist die bewußtlose Unendlichkeit. Die Kunst ist die Vereinigung des
Subjektiven mit dem Objektiven, – der Natur mit der Vernunft, der Subjektiven mit dem Objektiven, – der Natur mit der Vernunft, der
@@ -141,15 +141,15 @@ onUnmounted(() => {
heit: das Schöne hat nur als Geistiges wahrhaften Gehalt. Aber das heit: das Schöne hat nur als Geistiges wahrhaften Gehalt. Aber das
Geistige muß in der sinnlichen Form erscheinen. Die sinnliche Äu- ßerung Geistige muß in der sinnlichen Form erscheinen. Die sinnliche Äu- ßerung
des Geistes jedoch ist nur der Schein. Und dieser Schein ist die des Geistes jedoch ist nur der Schein. Und dieser Schein ist die
eigentliche Wirklichkeit des Schönen. Sodaß die Kunst die 46 eigentliche Wirklichkeit des Schönen. Sodaß die Kunst die Verwirklichung
Verwirklichung dieses Scheines der Idee und zugleich mit der Reli- gion dieses Scheines der Idee und zugleich mit der Reli- gion und Philosophie
und Philosophie das Mittel ist, die tiefsten Interessen der Men- schen das Mittel ist, die tiefsten Interessen der Men- schen und die
und die umfassendsten Wahrheiten des Geistes zum Bewußt- sein zu bringen umfassendsten Wahrheiten des Geistes zum Bewußt- sein zu bringen und
und auszusprechen. Wahrheit und Schönheit sind nach Hegel ein und auszusprechen. Wahrheit und Schönheit sind nach Hegel ein und dasselbe:
dasselbe: der Unterschied besteht nur darin, daß die Wahrheit die Idee der Unterschied besteht nur darin, daß die Wahrheit die Idee ist, wie
ist, wie diese an sich und dem allgemeinen Prinzip nach ist und als diese an sich und dem allgemeinen Prinzip nach ist und als solches
solches gedacht wird. Die Idee jedoch, die äußerlich existiert, wird für gedacht wird. Die Idee jedoch, die äußerlich existiert, wird für das
das Bewußtsein nicht blos wahr, son- dern auch schön. Das Schöne ist das Bewußtsein nicht blos wahr, son- dern auch schön. Das Schöne ist das
Scheinen der Idee Scheinen der Idee
</p> </p>
<p> <p>