added video project component

This commit is contained in:
liyunze 2025-06-21 23:13:43 +10:00
parent d2f28d2b7e
commit d612d159f0
4 changed files with 229 additions and 202 deletions

View file

@ -1,206 +1,65 @@
<script lang="ts">
import { onMount } from "svelte";
onMount(() => {
const videos = document.querySelectorAll<HTMLVideoElement>(".playable");
let userInteracted = false;
const handleUserInteraction = () => {
userInteracted = true;
document.removeEventListener("click", handleUserInteraction);
document.removeEventListener("keydown", handleUserInteraction);
};
document.addEventListener("click", handleUserInteraction);
document.addEventListener("keydown", handleUserInteraction);
videos.forEach((video) => {
// Ensure the video element supports play and pause methods
if (
typeof video.play === "function" &&
typeof video.pause === "function"
) {
video.addEventListener("mouseenter", () => {
if (userInteracted) {
video.volume = 0.2;
video.muted = false;
video.play();
} else {
video.volume = 0;
video.muted = true;
video.play();
}
});
video.addEventListener("mouseleave", () => {
video.pause();
video.currentTime = 0;
});
// Check if the screen width is less than or equal to 768px (mobile screen size)
if (window.innerWidth <= 768) {
video.muted = true;
video.autoplay = true;
video.loop = true;
video.play();
}
}
});
});
import VideoProject from "./VideoProject.svelte";
</script>
<div class="flex h-fit flex-col items-center pt-20 [perspective:1000px]">
<div class="w-full max-w-[1440px]">
<div
class="flex h-fit w-full flex-col justify-center px-10 lg:flex-row lg:px-20"
>
<div
class="image-container relative mb-5 flex aspect-video w-full [perspective:200px] lg:mr-5 lg:mb-0 lg:ml-auto lg:w-1/2"
>
<img
src="/images/projects/chess-opencv.webp"
alt="computer vision chess bot"
class="absolute top-1/2
left-1/2 max-w-full
[transform:rotateY(5deg)_rotateX(0deg)_translateZ(-50px)_translateX(-45%)_translateY(-50%)] rounded-xl
border-2 border-solid border-white opacity-40"
width="700"
/>
<!-- svelte-ignore a11y-media-has-caption -->
<video
width="700"
class="playable absolute top-1/2
left-1/2 max-w-full
[transform:rotateY(5deg)_rotateX(0deg)_translateZ(-50px)_translateX(-43%)_translateY(-52%)] rounded-xl
border-2 border-solid border-white opacity-100"
>
<source
src="/videos/chess_opencv_demo.webm"
type="video/webm"
/>
</video>
</div>
<div
class="flex h-fit w-full flex-col items-center justify-center lg:h-[340px] lg:w-1/2 lg:items-start lg:px-10"
>
<h2 class="mb-5 text-center text-3xl font-bold lg:text-left">
Computer Vision Chess bot
</h2>
<p
class="max-w-lg text-center text-xl text-balance lg:max-w-none lg:text-left"
>
A script that plays against opponents on lichess. Uses
OpenCV to detect the board and pieces, and the Stockfish
chess engine to determine the best moves. Then uses
PyAutoGUI to play the moves on the browser.
</p>
<div
class="mt-5 flex w-full flex-row flex-wrap justify-center lg:justify-start"
>
<div
class="my-1 mr-1 w-fit
rounded-full bg-[#3776AB] px-3 py-1 text-white
transition-colors duration-300"
>
Python
</div>
<div
class="my-1 mr-1 w-fit
rounded-full bg-[#3178C6] px-3 py-1 text-white
transition-colors duration-300"
>
OpenCV
</div>
<div
class="my-1 mr-1 w-fit
rounded-full bg-black px-3 py-1 text-white
transition-colors duration-300"
>
PyAutoGUI
</div>
</div>
</div>
</div>
<div
class="mt-10 flex h-fit w-full flex-col-reverse justify-center px-10 lg:flex-row lg:px-16"
>
<div
class="flex h-fit w-full flex-col items-center justify-center lg:h-[340px] lg:w-1/2 lg:items-end lg:px-10"
>
<h2 class="mb-5 text-center text-3xl font-bold lg:text-right">
Computer Vision AimLab Bot
</h2>
<p
class="max-w-lg text-center text-xl text-balance lg:max-w-none lg:text-right"
>
A script that plays AimLab, a game that helps improve your
aim in FPS games. Uses OpenCV to detect the targets and
win32API to move the mouse and click.
</p>
<div
class="mt-5 mb-10 flex w-full flex-row flex-wrap justify-center lg:mb-0 lg:justify-end"
>
<div
class="my-1 mr-1 w-fit
rounded-full bg-[#3776AB] px-3 py-1 text-white
transition-colors duration-300"
>
Python
</div>
<div
class="my-1 mr-1 w-fit
rounded-full bg-[#3178C6] px-3 py-1 text-white
transition-colors duration-300"
>
OpenCV
</div>
<div
class="my-1 mr-1 w-fit
rounded-full bg-[#0078D4] px-3 py-1 text-white
transition-colors duration-300"
>
win32API
</div>
</div>
</div>
<div
class="image-container relative mb-5 flex aspect-video w-full [perspective:200px] lg:mr-5 lg:mb-0 lg:ml-auto lg:w-1/2"
>
<img
src="/images/projects/aim-lab.webp"
alt="aim lab"
class="absolute top-1/2
left-1/2 max-w-full [transform:translateX(-48%)_translateY(-48%)_translateZ(-50px)_rotateY(-3deg)] rounded-xl
border-2 border-solid border-white opacity-40"
width="700"
/>
<!-- svelte-ignore a11y-media-has-caption -->
<video
width="700"
class="playable absolute top-1/2
left-1/2 max-w-full
[transform:translateX(-50%)_translateY(-50%)_translateZ(-50px)_rotateY(-3deg)] rounded-xl
border-2 border-solid border-white opacity-100"
><source src="/videos/aim_lab_demo.webm" /></video
>
</div>
</div>
<VideoProject
image_src="/images/projects/chess-opencv.webp"
image_alt="computer vision chess bot"
position="left"
video_src="/videos/chess_opencv_demo.webm"
title="Computer Vision Chess Bot"
description={`
A script that plays against opponents on lichess. Uses
OpenCV to detect the board and pieces, and the Stockfish
chess engine to determine the best moves. Then uses
PyAutoGUI to play the moves on the browser.
`}
tags={[
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "PyAutoGUI",
textColor: "white",
bgColor: "black",
},
]}
></VideoProject>
<VideoProject
image_src="/images/projects/aim-lab.webp"
image_alt="Computer Vision AimLab Bot"
position="right"
video_src="/videos/aim_lab_demo.webm"
title="Computer Vision AimLab Bot"
description={`
A script that plays AimLab, a game that helps improve your aim in FPS games. Uses OpenCV to detect the targets and win32API to move the mouse and click.
`}
tags={[
{
text: "Python",
textColor: "white",
bgColor: "var(--color-python)",
},
{
text: "OpenCV",
textColor: "white",
bgColor: "var(--color-opencv)",
},
{
text: "win32API",
textColor: "white",
bgColor: "var(--color-win32api)",
},
]}
></VideoProject>
</div>
</div>
<style>
.image-container img,
.image-container video {
transition: transform 0.5s ease-in-out;
}
@media (min-width: 1025px) {
.image-container:hover > img,
.image-container:hover > video {
transform: rotateY(0deg) rotateX(0deg) translateZ(0px)
translateX(-50%) translateY(-50%);
}
}
</style>

View file

@ -52,7 +52,7 @@ const leftProject = position == "left";
border-2 border-solid border-white rounded-xl`}
class:list={leftProject
? "[transform:translateX(50px)_translateY(5px)_rotateY(5deg)_rotateX(0deg)_translateZ(-50px)]"
: "[transform:translateZ(-50px)_rotateY(-3deg)]"}
: "[transform:translateX(10px)_translateY(5px)_translateZ(-50px)_rotateY(-3deg)]"}
width="700"
/>
<Image

View file

@ -47,6 +47,23 @@ import Project from "./Project.astro";
to add their own tasks to the widget for accountability!
Easy to setup and the customization possibilities are
endless!`}
tags={[
{
text: "HTML",
bgColor: "var(--color-html)",
textColor: "white",
},
{
text: "CSS",
bgColor: "var(--color-blue-600)",
textColor: "white",
},
{
text: "Javascript",
bgColor: "var(--color-yellow-500)",
textColor: "black",
},
]}
>
<TaskWidget
classList={`
@ -117,13 +134,15 @@ import Project from "./Project.astro";
<style is:global>
.image-container img,
.image-container .widget {
.image-container .widget,
.image-container video {
transition: transform 0.5s ease-in-out;
}
@media (min-width: 1025px) {
.image-container:hover > img,
.image-container:hover .widget {
.image-container:hover .widget,
.image-container:hover video {
transform: rotateY(0deg) rotateX(0deg) translateZ(0px) translateX(0)
translateY(0);
}

View file

@ -0,0 +1,149 @@
<script lang="ts">
import { onMount } from "svelte";
interface Tag {
text: string;
textColor: string;
bgColor: string;
}
interface Props {
position?: "left" | "right";
image_src: string;
image_alt: string;
video_src: string;
title: string;
description: string;
tags?: Tag[];
}
let {
position = "left",
image_src,
image_alt,
video_src,
title,
description,
tags = [],
}: Props = $props();
const positionLeft = position == "left";
onMount(() => {
const videos = document.querySelectorAll<HTMLVideoElement>(".playable");
let userInteracted = false;
const handleUserInteraction = () => {
userInteracted = true;
document.removeEventListener("click", handleUserInteraction);
document.removeEventListener("keydown", handleUserInteraction);
};
document.addEventListener("click", handleUserInteraction);
document.addEventListener("keydown", handleUserInteraction);
videos.forEach((video) => {
// Ensure the video element supports play and pause methods
if (
typeof video.play === "function" &&
typeof video.pause === "function"
) {
video.addEventListener("mouseenter", () => {
if (userInteracted) {
video.volume = 0.2;
video.muted = false;
video.play();
} else {
video.volume = 0;
video.muted = true;
video.play();
}
});
video.addEventListener("mouseleave", () => {
video.pause();
video.currentTime = 0;
});
// Check if the screen width is less than or equal to 768px (mobile screen size)
if (window.innerWidth <= 768) {
video.muted = true;
video.autoplay = true;
video.loop = true;
video.play();
}
}
});
});
</script>
<div
class="flex h-fit w-full flex-col justify-center px-10 lg:flex-row lg:px-20"
class:lg:flex-row={positionLeft}
class:lg:flex-row-reverse={!positionLeft}
>
<div
class="image-container relative mb-5 flex aspect-video w-full [perspective:200px] lg:mr-5 lg:mb-0 lg:ml-auto lg:w-1/2"
>
<img
src={image_src}
alt={image_alt}
class="absolute max-w-full
rounded-xl
border-2 border-solid border-white opacity-40
{positionLeft
? '[transform:rotateY(5deg)_translateX(50px)_translateY(5px)_translateZ(-50px)]'
: '[transform:rotateY(-5deg)_translateX(-20px)_translateY(5px)_translateZ(-50px)]'}"
width="700"
/>
<!-- svelte-ignore a11y_media_has_caption -->
<video
width="700"
class="playable absolute max-w-full
rounded-xl border-2 border-solid border-white
opacity-100
{positionLeft
? '[transform:rotateY(5deg)_translateX(60px)_translateZ(-50px)]'
: '[transform:rotateY(-5deg)_translateX(-30px)_translateZ(-50px)]'}"
>
<source src={video_src} type="video/webm" />
</video>
</div>
<div
class="flex h-fit w-full flex-col items-center justify-center lg:h-[340px] lg:w-1/2 lg:px-10
{positionLeft ? 'lg:items-start' : 'lg:items-end'}"
>
<h2
class="mb-5 text-center text-3xl font-bold
{positionLeft ? 'lg:text-left' : 'lg:text-right'}"
>
{title}
</h2>
<p
class="max-w-lg text-center text-xl text-balance lg:max-w-none
{positionLeft ? 'lg:text-left' : 'lg:text-right'}"
>
{description}
</p>
<div
class="mt-5 flex w-full flex-row flex-wrap justify-center
{positionLeft ? 'lg:justify-start' : 'lg:justify-end'}"
>
{#each tags as tag}
<div
class="my-1 mr-1 w-fit
rounded-full px-3 py-1
transition-colors duration-300"
style={`
background-color: ${tag.bgColor || "black"};
color: ${tag.textColor || "white"};
`}
>
{tag.text}
</div>
{/each}
</div>
</div>
</div>