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"> <script lang="ts">
import { onMount } from "svelte"; import VideoProject from "./VideoProject.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();
}
}
});
});
</script> </script>
<div class="flex h-fit flex-col items-center pt-20 [perspective:1000px]"> <div class="flex h-fit flex-col items-center pt-20 [perspective:1000px]">
<div class="w-full max-w-[1440px]"> <div class="w-full max-w-[1440px]">
<div <VideoProject
class="flex h-fit w-full flex-col justify-center px-10 lg:flex-row lg:px-20" image_src="/images/projects/chess-opencv.webp"
> image_alt="computer vision chess bot"
<div position="left"
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" video_src="/videos/chess_opencv_demo.webm"
> title="Computer Vision Chess Bot"
<img description={`
src="/images/projects/chess-opencv.webp" A script that plays against opponents on lichess. Uses
alt="computer vision chess bot" OpenCV to detect the board and pieces, and the Stockfish
class="absolute top-1/2 chess engine to determine the best moves. Then uses
left-1/2 max-w-full PyAutoGUI to play the moves on the browser.
[transform:rotateY(5deg)_rotateX(0deg)_translateZ(-50px)_translateX(-45%)_translateY(-50%)] rounded-xl `}
border-2 border-solid border-white opacity-40" tags={[
width="700" {
/> text: "Python",
textColor: "white",
<!-- svelte-ignore a11y-media-has-caption --> bgColor: "var(--color-python)",
<video },
width="700" {
class="playable absolute top-1/2 text: "OpenCV",
left-1/2 max-w-full textColor: "white",
[transform:rotateY(5deg)_rotateX(0deg)_translateZ(-50px)_translateX(-43%)_translateY(-52%)] rounded-xl bgColor: "var(--color-opencv)",
border-2 border-solid border-white opacity-100" },
> {
<source text: "PyAutoGUI",
src="/videos/chess_opencv_demo.webm" textColor: "white",
type="video/webm" bgColor: "black",
/> },
</video> ]}
</div> ></VideoProject>
<div <VideoProject
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" image_src="/images/projects/aim-lab.webp"
> image_alt="Computer Vision AimLab Bot"
<h2 class="mb-5 text-center text-3xl font-bold lg:text-left"> position="right"
Computer Vision Chess bot video_src="/videos/aim_lab_demo.webm"
</h2> title="Computer Vision AimLab Bot"
<p description={`
class="max-w-lg text-center text-xl text-balance lg:max-w-none lg:text-left" 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.
> `}
A script that plays against opponents on lichess. Uses tags={[
OpenCV to detect the board and pieces, and the Stockfish {
chess engine to determine the best moves. Then uses text: "Python",
PyAutoGUI to play the moves on the browser. textColor: "white",
</p> bgColor: "var(--color-python)",
<div },
class="mt-5 flex w-full flex-row flex-wrap justify-center lg:justify-start" {
> text: "OpenCV",
<div textColor: "white",
class="my-1 mr-1 w-fit bgColor: "var(--color-opencv)",
rounded-full bg-[#3776AB] px-3 py-1 text-white },
transition-colors duration-300" {
> text: "win32API",
Python textColor: "white",
</div> bgColor: "var(--color-win32api)",
<div },
class="my-1 mr-1 w-fit ]}
rounded-full bg-[#3178C6] px-3 py-1 text-white ></VideoProject>
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>
</div> </div>
</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`} border-2 border-solid border-white rounded-xl`}
class:list={leftProject class:list={leftProject
? "[transform:translateX(50px)_translateY(5px)_rotateY(5deg)_rotateX(0deg)_translateZ(-50px)]" ? "[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" width="700"
/> />
<Image <Image

View file

@ -47,6 +47,23 @@ import Project from "./Project.astro";
to add their own tasks to the widget for accountability! to add their own tasks to the widget for accountability!
Easy to setup and the customization possibilities are Easy to setup and the customization possibilities are
endless!`} 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 <TaskWidget
classList={` classList={`
@ -117,13 +134,15 @@ import Project from "./Project.astro";
<style is:global> <style is:global>
.image-container img, .image-container img,
.image-container .widget { .image-container .widget,
.image-container video {
transition: transform 0.5s ease-in-out; transition: transform 0.5s ease-in-out;
} }
@media (min-width: 1025px) { @media (min-width: 1025px) {
.image-container:hover > img, .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) transform: rotateY(0deg) rotateX(0deg) translateZ(0px) translateX(0)
translateY(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>