Skip to content

Commit

Permalink
Add cube to hero background
Browse files Browse the repository at this point in the history
  • Loading branch information
timsexperiments committed Jun 12, 2024
1 parent ecbfb83 commit c15b2b2
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 20 deletions.
3 changes: 3 additions & 0 deletions apps/timsexperiments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@timsexperiments/theme": "^0.0.0",
"@timsexperiments/three-rubiks-cube": "^0.0.3",
"@timsexperiments/views-client": "workspace:*",
"@types/hast": "^3.0.4",
"@types/react": "^18.2.47",
Expand All @@ -44,12 +45,14 @@
"tailwind-merge": "^2.2.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"three": "^0.165.0",
"typescript": "^5.3.3",
"unist-util-visit": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@types/three": "^0.165.0",
"pnpm": "^9.3.0",
"prettier": "^3.2.2",
"prettier-plugin-astro": "^0.12.3",
Expand Down
29 changes: 10 additions & 19 deletions apps/timsexperiments/src/components/home/Hero.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
---
import laptopDark from '@/assets/laptop_dark.jpg';
import laptopLight from '@/assets/laptop_light.jpg';
import { Image } from 'astro:assets';
import { Brain, Hammer, Microscope } from 'lucide-react';
import ButtonLink from '../ButtonLink.astro';
import RubiksCubeControl from './hero/RubiksCubeControl.astro';
---

<div
Expand Down Expand Up @@ -46,20 +44,13 @@ import ButtonLink from '../ButtonLink.astro';
</div>
</div>
<div class="relative hidden h-full w-full lg:block">
<Image
class="absolute h-full w-full object-cover object-left-top opacity-90 dark:hidden"
src={laptopLight}
alt="Tim's Experiments :)"
format="webp"
width="1000"
height="1000"
/>
<Image
class="absolute hidden h-full w-full object-cover object-left-top opacity-90 dark:block"
src={laptopDark}
alt="Tim's Experiments :)"
format="webp"
width="1000"
height="1000"
/>
<div class="absolute flex h-full items-center">
<RubiksCubeControl />
</div>
<canvas
id="cube"
class="absolute left-0 top-0 -z-10 h-full w-full -translate-x-[20%]"
></canvas>
</div>

<script src="@/scripts/cube.ts"></script>
17 changes: 17 additions & 0 deletions apps/timsexperiments/src/components/home/hero/Key.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { cn } from '@/lib/utils';
import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'div'>;
const { class: className, ...props } = Astro.props;
---

<div
class={cn(
'flex h-8 w-8 items-center justify-center bg-rhino-700 p-2 font-mono text-sm text-rhino-50 shadow-xl',
className
)}
{...props}>
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import Key from '@/components/home/hero/Key.astro';
---

<div id="cube-controls" class="hidden w-fit -translate-x-4 flex-col gap-1">
<div class="flex items-center justify-center">
<Key id="cube-w-control">W</Key>
</div>
<div class="flex items-center justify-center gap-1">
<Key id="cube-a-control">A</Key>
<Key id="cube-s-control">S</Key>
<Key id="cube-d-control">D</Key>
</div>
</div>
1 change: 0 additions & 1 deletion apps/timsexperiments/src/scripts/animate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ const ANIMATE_IN_VIEWPORT_SELECTOR = '.animate-in-viewport';

function setupAnimateStartInViewport() {
const observer = new IntersectionObserver((entries) => {
console.log('found:', entries);
entries.forEach((entry) => {
const target = entry.target as HTMLElement;
const animation = target.dataset.animation;
Expand Down
145 changes: 145 additions & 0 deletions apps/timsexperiments/src/scripts/cube.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { RubiksCube } from '@timsexperiments/three-rubiks-cube';
import { explodeAndReassemble } from '@timsexperiments/three-rubiks-cube/async';
import * as THREE from 'three';

const isDark = document.documentElement.classList.contains('dark');

const canvas = document.querySelector<HTMLCanvasElement>('canvas#cube')!;
const sizes = {
width: window.innerWidth / 1.5,
height: window.innerHeight,
get aspect() {
return this.height !== 0 ? this.width / this.height : 0;
},
};

window.addEventListener('resize', () => {
sizes.width = window.innerWidth / 1.5;
sizes.height = window.innerHeight;

camera.aspect = sizes.aspect;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(window.devicePixelRatio);
});

const scene = new THREE.Scene();
scene.background = isDark
? new THREE.Color(0x0a0b0a)
: new THREE.Color(0xfafafa);

const camera = new THREE.PerspectiveCamera(75, sizes.aspect, 0.1, 500);

camera.position.z = 7;
camera.position.y = 4;
camera.position.x = -4;

const cube = new RubiksCube(camera, canvas, {
colors: [0xfafafa, 0xf8e749, 0x1772b8, 0x17b897, 0xb82217, 0xe59e19],
});
scene.add(cube);
cube.castShadow = true;
cube.shuffle(15, {
onComplete: async () => {
await explodeAndReassemble(cube, { range: 7 });
await showWhatControlsDo();
},
});
camera.lookAt(cube.position);

const AmbientLight = new THREE.AmbientLight(0xffffff, 1);
scene.add(AmbientLight);

const spotlight = new THREE.PointLight(0xffffff, 50);
scene.add(spotlight);

spotlight.position.set(-2, 8, 4);

spotlight.castShadow = true;

spotlight.shadow.mapSize.width = 1024;
spotlight.shadow.mapSize.height = 1024;
spotlight.shadow.camera.near = 5;
spotlight.shadow.camera.far = 10;

const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

scene.environment = new THREE.PMREMGenerator(renderer).fromScene(scene).texture;

function tick() {
renderer.render(scene, camera);

window.requestAnimationFrame(tick);
}

tick();

const observer = new MutationObserver((mutations) => {
if (!mutations.length) return;

const mutation = mutations[0];
const { target } = mutation;

if (mutation.attributeName === 'class') {
const isDark = (target as HTMLElement).classList.contains('dark');
if (isDark) {
scene.background = new THREE.Color(0x0a0b0a);
} else {
scene.background = new THREE.Color(0xfafafa);
}
}
});

observer.observe(document.documentElement, {
attributes: true,
});

async function showWhatControlsDo() {
const controls = document.querySelector<HTMLDivElement>('#cube-controls')!;
const wControl = document.querySelector<HTMLDivElement>('#cube-w-control')!;
const aControl = document.querySelector<HTMLDivElement>('#cube-a-control')!;
const sControl = document.querySelector<HTMLDivElement>('#cube-s-control')!;
const dControl = document.querySelector<HTMLDivElement>('#cube-d-control')!;

controls.classList.remove('hidden');
controls.classList.add('flex');

wControl.classList.add('animate-pulse', 'repeat-1');
await wait(1000);
cube.rotateCube('x', 'clockwise', {
onComplete: async () => {
await wait(1000);
aControl.classList.add('animate-pulse', 'repeat-1');
await wait(1000);
cube.rotateCube('y', 'clockwise', {
onComplete: async () => {
await wait(1000);
sControl.classList.add('animate-pulse', 'repeat-1');
await wait(1000);
cube.rotateCube('x', 'counterclockwise', {
onComplete: async () => {
await wait(1000);
dControl.classList.add('animate-pulse', 'repeat-1');
await wait(1000);
cube.rotateCube('y', 'counterclockwise', {
onComplete: async () => {
await wait(1000);
controls.classList.add('hidden');
controls.classList.remove('flex');
},
});
},
});
},
});
},
});
}

function wait(timeout: number = 1000) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
Loading

0 comments on commit c15b2b2

Please sign in to comment.