get routing working, add index for poetry, list first 6 poems on index, let each take you to their page with the poem's content
This commit is contained in:
41
src/lib/components/scenes/app/App.svelte
Normal file
41
src/lib/components/scenes/app/App.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { T, useTask, useThrelte } from '@threlte/core';
|
||||
import { World } from '@threlte/rapier';
|
||||
import { Inspector } from 'three-inspect';
|
||||
import { dev } from '$app/environment';
|
||||
import SpaceSkysphere from './SpaceSkysphere.svelte';
|
||||
import { Group, type Object3DEventMap } from 'three';
|
||||
import DollyCam from './DollyCam.svelte';
|
||||
import Planet from '$lib/components/scenes/app/Planet.svelte';
|
||||
|
||||
const { invalidate } = useThrelte();
|
||||
let spaceObjects: Group<Object3DEventMap>;
|
||||
|
||||
useTask(
|
||||
'updateSpaceObjects',
|
||||
(delta) => {
|
||||
if (!spaceObjects) return;
|
||||
spaceObjects.rotation.y += 0.3 * delta;
|
||||
spaceObjects.position.y = Math.sin(spaceObjects.rotation.y);
|
||||
|
||||
invalidate();
|
||||
},
|
||||
{ autoInvalidate: false }
|
||||
);
|
||||
</script>
|
||||
|
||||
<World gravity={[0, 0, 0]}>
|
||||
<DollyCam />
|
||||
|
||||
<T.Group bind:ref={spaceObjects}>
|
||||
<SpaceSkysphere size={100} count={500} />
|
||||
|
||||
<Planet />
|
||||
|
||||
<slot />
|
||||
</T.Group>
|
||||
</World>
|
||||
|
||||
{#if false}
|
||||
<Inspector />
|
||||
{/if}
|
26
src/lib/components/scenes/app/CanvasContainer.svelte
Normal file
26
src/lib/components/scenes/app/CanvasContainer.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<!-- src/lib/CanvasLayout.svelte -->
|
||||
<script lang="ts">
|
||||
import { Canvas } from '@threlte/core';
|
||||
</script>
|
||||
|
||||
<div class="canvas">
|
||||
<Canvas>
|
||||
<slot />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(0, 36, 6);
|
||||
background: linear-gradient(180deg, rgba(0, 36, 6, 1) 0%, rgba(0, 0, 0, 1) 100%);
|
||||
position: absolute;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
44
src/lib/components/scenes/app/DollyCam.svelte
Normal file
44
src/lib/components/scenes/app/DollyCam.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { T, useTask, useThrelte } from '@threlte/core';
|
||||
import { onMount } from 'svelte';
|
||||
import { PerspectiveCamera, Vector3 } from 'three';
|
||||
const { invalidate } = useThrelte();
|
||||
|
||||
let camLookatPosition: Vector3 = new Vector3();
|
||||
let camCurrentPosition: Vector3 = new Vector3();
|
||||
let camDamping: number = 1;
|
||||
let camera: PerspectiveCamera;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
// normalize the mouse position to [-1, 1], and smoothly interpolate the camera's lookAt position
|
||||
camLookatPosition.set(
|
||||
(event.clientX / window.innerWidth) * 2 - 1,
|
||||
-(event.clientY / window.innerHeight) * 2 + 1,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
useTask(
|
||||
'dollyCam',
|
||||
(delta) => {
|
||||
if (!camera) return;
|
||||
camCurrentPosition.lerp(camLookatPosition, camDamping * delta);
|
||||
camera.lookAt(camCurrentPosition);
|
||||
|
||||
if (camera && camCurrentPosition.distanceTo(camLookatPosition) > 0.1) {
|
||||
invalidate();
|
||||
}
|
||||
},
|
||||
{ autoInvalidate: false }
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<T.PerspectiveCamera position.z={25} makeDefault fov={50} far={10000} bind:ref={camera} />
|
51
src/lib/components/scenes/app/MenuItem.svelte
Normal file
51
src/lib/components/scenes/app/MenuItem.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { HTML } from '@threlte/extras';
|
||||
import { Attractor } from '@threlte/rapier';
|
||||
|
||||
export let position: [number, number, number] = [0, 0, 0];
|
||||
export let range: number = 100;
|
||||
export let clickHandler: (() => void) | undefined = undefined;
|
||||
export let htmlContent: HTMLElement | string = 'Hello!';
|
||||
export let href: string = '/';
|
||||
|
||||
let isHovering = false;
|
||||
let isPointerDown = false;
|
||||
|
||||
const onClick = () => {
|
||||
console.log('clicked!');
|
||||
if (clickHandler) {
|
||||
clickHandler();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<HTML position.x={position[0]} position.y={position[1]} position.z={position[2]}>
|
||||
<a
|
||||
{href}
|
||||
on:pointerenter={() => (isHovering = true)}
|
||||
on:pointerleave={() => {
|
||||
isPointerDown = false;
|
||||
isHovering = false;
|
||||
}}
|
||||
on:pointerdown={() => (isPointerDown = true)}
|
||||
on:pointerup={() => (isPointerDown = false)}
|
||||
on:pointercancel={() => {
|
||||
isPointerDown = false;
|
||||
isHovering = false;
|
||||
}}
|
||||
on:click={onClick}
|
||||
class="bg-green-700 px-3 py-3 text-white opacity-50 hover:opacity-90 active:opacity-100"
|
||||
style="transform: translate(-50%, 50%); display: block;"
|
||||
>
|
||||
{htmlContent}
|
||||
</a>
|
||||
</HTML>
|
||||
|
||||
<Attractor
|
||||
position.x={position[0]}
|
||||
position.y={position[1]}
|
||||
position.z={position[2]}
|
||||
{range}
|
||||
strength={isHovering ? 1 : 0}
|
||||
gravityType={'static'}
|
||||
/>
|
19
src/lib/components/scenes/app/Planet.svelte
Normal file
19
src/lib/components/scenes/app/Planet.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" context="module">
|
||||
import { T } from '@threlte/core';
|
||||
import { Attractor, Collider, RigidBody } from '@threlte/rapier';
|
||||
import { MeshBasicMaterial, SphereGeometry } from 'three';
|
||||
const geometry = new SphereGeometry(1, 8, 4);
|
||||
const material = new MeshBasicMaterial({ color: '#339933', wireframe: true });
|
||||
const position: [number, number, number] = [0, 0, 0];
|
||||
const scale: number = 2;
|
||||
const offset = position.map((el) => el + 1.5);
|
||||
</script>
|
||||
|
||||
<Attractor {position} range={100} strength={0.3} gravityType={'linear'} />
|
||||
|
||||
<T.Group position.x={-offset[0]} position.y={-offset[1]} position.z={offset[2]}>
|
||||
<RigidBody linearDamping={0.3}>
|
||||
<Collider shape="ball" args={[scale]} mass={scale} />
|
||||
<T.Mesh scale={[scale, scale, scale]} {geometry} {material} />
|
||||
</RigidBody>
|
||||
</T.Group>
|
@@ -5,7 +5,7 @@
|
||||
import { Studio } from '@threlte/theatre';
|
||||
</script>
|
||||
|
||||
<Studio enabled={false} />
|
||||
<Studio enabled={dev} />
|
||||
|
||||
<Canvas>
|
||||
<Spinners />
|
||||
|
@@ -8,18 +8,18 @@
|
||||
|
||||
let sequence: SequenceController;
|
||||
|
||||
let box: Mesh;
|
||||
let ball: Mesh;
|
||||
let spotlight: SpotLight;
|
||||
let lastBoxPosition = new Vector3();
|
||||
let currentBoxPosition = new Vector3();
|
||||
let lastBallPosition = new Vector3();
|
||||
let currentBallPosition = new Vector3();
|
||||
let config = spinnersJson as IProjectConfig;
|
||||
|
||||
const boxMoved: any = (props: { position: { x: number; y: number; z: number } }) => {
|
||||
if (box && spotlight) {
|
||||
const ballMoved: any = (props: { position: { x: number; y: number; z: number } }) => {
|
||||
if (ball && spotlight) {
|
||||
const { x, y, z } = props.position;
|
||||
currentBoxPosition.set(x, y, z);
|
||||
spotlight.lookAt(currentBoxPosition);
|
||||
lastBoxPosition.copy(currentBoxPosition);
|
||||
currentBallPosition.set(x, y, z);
|
||||
spotlight.lookAt(currentBallPosition);
|
||||
lastBallPosition.copy(currentBallPosition);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -33,10 +33,10 @@
|
||||
<!-- create a T.SpotLight that looks at box-->
|
||||
<T.SpotLight position={[0, 5, 3]} intensity={10} bind:ref={spotlight}></T.SpotLight>
|
||||
|
||||
<SheetObject key="Box" let:Transform let:Sync on:change={boxMoved}>
|
||||
<SheetObject key="Box" let:Transform let:Sync on:change={ballMoved}>
|
||||
<Transform>
|
||||
<T.Mesh position.y={0.5} bind:ref={box}>
|
||||
<T.BoxGeometry args={[1, 1, 1]} />
|
||||
<T.Mesh position.y={0.5} bind:ref={ball}>
|
||||
<T.SphereGeometry args={[1, 8, 4]} />
|
||||
<T.MeshStandardMaterial color="#b00d03">
|
||||
<Sync color roughness metalness />
|
||||
</T.MeshStandardMaterial>
|
||||
|
@@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Canvas, T } from '@threlte/core';
|
||||
import { OrbitControls } from '@threlte/extras';
|
||||
import OrbitingSpheres from '$lib/components/scenes/home/OrbitingSpheres.svelte';
|
||||
import { World } from '@threlte/rapier';
|
||||
import { Inspector } from 'three-inspect';
|
||||
import { dev } from '$app/environment';
|
||||
import SpaceSkysphere from './SpaceSkysphere.svelte';
|
||||
</script>
|
||||
|
||||
<Canvas>
|
||||
<World gravity={[0, 0, 0]}>
|
||||
<T.PerspectiveCamera position.y={15} position.z={25} makeDefault fov={70} far={10000}>
|
||||
<OrbitControls
|
||||
enableZoom={false}
|
||||
enablePan={false}
|
||||
enableRotate={false}
|
||||
enableDamping
|
||||
target.y={0}
|
||||
autoRotate
|
||||
/>
|
||||
</T.PerspectiveCamera>
|
||||
|
||||
<SpaceSkysphere size={100} count={500} />
|
||||
|
||||
<OrbitingSpheres position={[0, 0, 0]} range={10} />
|
||||
</World>
|
||||
|
||||
{#if dev}
|
||||
<Inspector />
|
||||
{/if}
|
||||
</Canvas>
|
@@ -1,86 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
const geometry = new SphereGeometry(1, 8, 4);
|
||||
const material = new MeshBasicMaterial({ color: '#339933' });
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { T } from '@threlte/core';
|
||||
import { HTML } from '@threlte/extras';
|
||||
import { Attractor, Collider, RigidBody } from '@threlte/rapier';
|
||||
import { MeshBasicMaterial, SphereGeometry, Vector3 } from 'three';
|
||||
|
||||
export let range: number = 10;
|
||||
export let position: [number, number, number] = [0, 0, 0];
|
||||
let isHovering = false;
|
||||
let isPointerDown = false;
|
||||
const bodyPositions = [
|
||||
new Vector3(position[0] - range, position[0] - range, position[0] - range).toArray(),
|
||||
new Vector3(position[1] + range / 2, position[1] + range, position[1] + range).toArray(),
|
||||
new Vector3(position[2] + range, position[2] - range / 2, position[2] + range).toArray()
|
||||
];
|
||||
|
||||
const getId = () => {
|
||||
return Math.random().toString(16).slice(2);
|
||||
};
|
||||
|
||||
const getRandomSize = (): number => {
|
||||
return Math.random() / 4 + 0.25;
|
||||
};
|
||||
|
||||
const generateBodies = (c: number) => {
|
||||
return Array(c)
|
||||
.fill('x')
|
||||
.map((_, index) => {
|
||||
return {
|
||||
id: getId(),
|
||||
position: bodyPositions[index],
|
||||
size: getRandomSize()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
console.log('clicked');
|
||||
};
|
||||
|
||||
$: bodies = generateBodies(2);
|
||||
</script>
|
||||
|
||||
<HTML position.x={position[0]} position.y={position[1]} position.z={position[2]}>
|
||||
<button
|
||||
on:pointerenter={() => (isHovering = true)}
|
||||
on:pointerleave={() => {
|
||||
isPointerDown = false;
|
||||
isHovering = false;
|
||||
}}
|
||||
on:pointerdown={() => (isPointerDown = true)}
|
||||
on:pointerup={() => (isPointerDown = false)}
|
||||
on:pointercancel={() => {
|
||||
isPointerDown = false;
|
||||
isHovering = false;
|
||||
}}
|
||||
on:click={onClick}
|
||||
class="rounded-full bg-orange-500 px-3 text-white hover:opacity-90 active:opacity-70"
|
||||
>
|
||||
Hello!
|
||||
</button>
|
||||
</HTML>
|
||||
|
||||
<Attractor
|
||||
position.x={position[0]}
|
||||
position.y={position[1]}
|
||||
position.z={position[2]}
|
||||
range={200}
|
||||
strength={isHovering ? 1 : 0.1}
|
||||
gravityType={'linear'}
|
||||
/>
|
||||
|
||||
{#each bodies as body (body.id)}
|
||||
<T.Group position={body.position}>
|
||||
<RigidBody>
|
||||
<Collider shape="ball" args={[body.size]} mass={body.size} />
|
||||
<Attractor range={50} strength={1} gravityType={'newtonian'} />
|
||||
<T.Mesh scale={[body.size, body.size, body.size]} {geometry} {material} />
|
||||
</RigidBody>
|
||||
</T.Group>
|
||||
{/each}
|
63
src/lib/utils/DomPortal.svelte
Normal file
63
src/lib/utils/DomPortal.svelte
Normal file
@@ -0,0 +1,63 @@
|
||||
<script context="module">
|
||||
import { tick } from 'svelte';
|
||||
|
||||
/**
|
||||
* Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @param {HTMLElement|string} target DOM Element or CSS Selector
|
||||
*/
|
||||
export function portal(el, target = 'body') {
|
||||
let targetEl;
|
||||
/**
|
||||
* @param {string | HTMLElement} newTarget
|
||||
*/
|
||||
async function update(newTarget) {
|
||||
target = newTarget;
|
||||
if (typeof target === 'string') {
|
||||
targetEl = document.querySelector(target);
|
||||
if (targetEl === null) {
|
||||
await tick();
|
||||
targetEl = document.querySelector(target);
|
||||
}
|
||||
if (targetEl === null) {
|
||||
throw new Error(`No element found matching css selector: "${target}"`);
|
||||
}
|
||||
} else if (target instanceof HTMLElement) {
|
||||
targetEl = target;
|
||||
} else {
|
||||
throw new TypeError(
|
||||
`Unknown portal target type: ${
|
||||
target === null ? 'null' : typeof target
|
||||
}. Allowed types: string (CSS selector) or HTMLElement.`
|
||||
);
|
||||
}
|
||||
targetEl.appendChild(el);
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
update(target);
|
||||
return {
|
||||
update,
|
||||
destroy
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* DOM Element or CSS Selector
|
||||
* @type { HTMLElement|string}
|
||||
*/
|
||||
export let target = 'body';
|
||||
</script>
|
||||
|
||||
<div use:portal={target} hidden style="display: contents;">
|
||||
<slot />
|
||||
</div>
|
80
src/lib/utils/index.ts
Normal file
80
src/lib/utils/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
date: string;
|
||||
content: string;
|
||||
categories?: string[];
|
||||
section?: SectionKey;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
poetry: 'poetry';
|
||||
thoughts: 'thoughts';
|
||||
projects: 'projects';
|
||||
}
|
||||
|
||||
type SectionKey = keyof Section;
|
||||
|
||||
export interface Post {
|
||||
meta: Metadata;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
function isData(obj: unknown): obj is Data {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataObj = obj as Data;
|
||||
|
||||
return (
|
||||
'metadata' in dataObj &&
|
||||
typeof dataObj.metadata.title === 'string' &&
|
||||
typeof dataObj.metadata.date === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export const fetchMarkdownPosts = async (
|
||||
section: SectionKey,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<Post[]> => {
|
||||
let posts: Record<string, () => Promise<unknown>>;
|
||||
switch (section) {
|
||||
case 'poetry':
|
||||
posts = import.meta.glob('/src/posts/poetry/*.md');
|
||||
break;
|
||||
case 'projects':
|
||||
posts = import.meta.glob('/src/routes/(app)/projects/posts/*.md');
|
||||
break;
|
||||
case 'thoughts':
|
||||
posts = import.meta.glob('/src/routes/(app)/thoughts/posts/*.md');
|
||||
break;
|
||||
default:
|
||||
throw new Error('Could not find this section');
|
||||
}
|
||||
const iterablePostFiles = Object.entries(posts);
|
||||
|
||||
const allPosts = await Promise.all(
|
||||
iterablePostFiles.map(async ([path, resolver]) => {
|
||||
const data = await resolver();
|
||||
if (isData(data)) {
|
||||
const { metadata } = data;
|
||||
const postPath = path.slice(11, -3);
|
||||
return {
|
||||
meta: { ...metadata, section: section },
|
||||
path: postPath
|
||||
};
|
||||
} else {
|
||||
throw new Error('Could not properly parse this post');
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const paginatedPosts = allPosts.slice(offset, offset + limit);
|
||||
|
||||
return paginatedPosts;
|
||||
};
|
Reference in New Issue
Block a user