Initial commit

This commit is contained in:
2026-01-30 03:53:38 +07:00
commit 42abc57430
85 changed files with 8922 additions and 0 deletions

18
src/components/About.tsx Normal file
View File

@@ -0,0 +1,18 @@
import "./styles/About.css";
const About = () => {
return (
<div className="about-section" id="about">
<div className="about-me">
<h3 className="title">About Me</h3>
<p className="para">
QA & Automation Engineer specializing in manual, API, and database testing.
Experienced in improving release stability and building automated workflows.
Physics student with a CS minor, focused on creating robust testing frameworks.
</p>
</div>
</div>
);
};
export default About;

65
src/components/Career.tsx Normal file
View File

@@ -0,0 +1,65 @@
import "./styles/Career.css";
const Career = () => {
return (
<div className="career-section section-container">
<div className="career-container">
<h2>
My career <span>&</span>
<br /> experience
</h2>
<div className="career-info">
<div className="career-timeline">
<div className="career-dot"></div>
</div>
<div className="career-info-box">
<div className="career-info-in">
<div className="career-role">
<h4>Software Testing Engineer</h4>
<h5>LACA CITY COMPANY</h5>
</div>
<h3>2024</h3>
</div>
<p>
Performed API and database testing to ensure system stability for a parking management
platform serving 5,000+ users. Detected 40+ functional/UI issues prior to release,
contributing to a 20% improvement in release stability. Collaborated with developers
using Jira, Postman, and Chrome DevTools.
</p>
</div>
<div className="career-info-box">
<div className="career-info-in">
<div className="career-role">
<h4>Head of Software Development</h4>
<h5>VIETNAM SPACE INDUSTRY JSC</h5>
</div>
<h3>2025</h3>
</div>
<p>
Designed and executed 150+ test cases in TestRail achieving &gt;98% release stability.
Tracked 70+ bug tickets on Jira, reducing issue resolution time by 25%. Proactively
adopted automation workflows using n8n, Postman, and Swagger to improve test efficiency
and coverage.
</p>
</div>
<div className="career-info-box">
<div className="career-info-in">
<div className="career-role">
<h4>QA & Automation Engineer</h4>
<h5>CURRENT ROLE</h5>
</div>
<h3>NOW</h3>
</div>
<p>
Continuing to grow as an Automation Testing specialist, designing robust frameworks
and accelerating release cycles. Leading quality control initiatives for NASA International
Space Apps Challenge and Conference Earth Sciences events with 500+ participants.
</p>
</div>
</div>
</div>
</div>
);
};
export default Career;

View File

@@ -0,0 +1,160 @@
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import setCharacter from "./utils/character";
import setLighting from "./utils/lighting";
import { useLoading } from "../../context/LoadingProvider";
import handleResize from "./utils/resizeUtils";
import {
handleMouseMove,
handleTouchEnd,
handleHeadRotation,
handleTouchMove,
} from "./utils/mouseUtils";
import setAnimations from "./utils/animationUtils";
import { setProgress } from "../Loading";
const Scene = () => {
const canvasDiv = useRef<HTMLDivElement | null>(null);
const hoverDivRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef(new THREE.Scene());
const { setLoading } = useLoading();
const [character, setChar] = useState<THREE.Object3D | null>(null);
useEffect(() => {
if (canvasDiv.current) {
let rect = canvasDiv.current.getBoundingClientRect();
let container = { width: rect.width, height: rect.height };
const aspect = container.width / container.height;
const scene = sceneRef.current;
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
});
renderer.setSize(container.width, container.height);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
canvasDiv.current.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(14.5, aspect, 0.1, 1000);
camera.position.z = 10;
camera.position.set(0, 13.1, 24.7);
camera.zoom = 1.1;
camera.updateProjectionMatrix();
let headBone: THREE.Object3D | null = null;
let screenLight: any | null = null;
let mixer: THREE.AnimationMixer;
const clock = new THREE.Clock();
const light = setLighting(scene);
let progress = setProgress((value) => setLoading(value));
const { loadCharacter } = setCharacter(renderer, scene, camera);
loadCharacter().then((gltf) => {
if (gltf) {
const animations = setAnimations(gltf);
hoverDivRef.current && animations.hover(gltf, hoverDivRef.current);
mixer = animations.mixer;
let character = gltf.scene;
setChar(character);
scene.add(character);
headBone = character.getObjectByName("spine006") || null;
screenLight = character.getObjectByName("screenlight") || null;
progress.loaded().then(() => {
setTimeout(() => {
light.turnOnLights();
animations.startIntro();
}, 2500);
});
window.addEventListener("resize", () =>
handleResize(renderer, camera, canvasDiv, character)
);
}
});
let mouse = { x: 0, y: 0 },
interpolation = { x: 0.1, y: 0.2 };
const onMouseMove = (event: MouseEvent) => {
handleMouseMove(event, (x, y) => (mouse = { x, y }));
};
let debounce: number | undefined;
const onTouchStart = (event: TouchEvent) => {
const element = event.target as HTMLElement;
debounce = setTimeout(() => {
element?.addEventListener("touchmove", (e: TouchEvent) =>
handleTouchMove(e, (x, y) => (mouse = { x, y }))
);
}, 200);
};
const onTouchEnd = () => {
handleTouchEnd((x, y, interpolationX, interpolationY) => {
mouse = { x, y };
interpolation = { x: interpolationX, y: interpolationY };
});
};
document.addEventListener("mousemove", (event) => {
onMouseMove(event);
});
const landingDiv = document.getElementById("landingDiv");
if (landingDiv) {
landingDiv.addEventListener("touchstart", onTouchStart);
landingDiv.addEventListener("touchend", onTouchEnd);
}
const animate = () => {
requestAnimationFrame(animate);
if (headBone) {
handleHeadRotation(
headBone,
mouse.x,
mouse.y,
interpolation.x,
interpolation.y,
THREE.MathUtils.lerp
);
light.setPointLight(screenLight);
}
const delta = clock.getDelta();
if (mixer) {
mixer.update(delta);
}
renderer.render(scene, camera);
};
animate();
return () => {
clearTimeout(debounce);
scene.clear();
renderer.dispose();
window.removeEventListener("resize", () =>
handleResize(renderer, camera, canvasDiv, character!)
);
if (canvasDiv.current) {
canvasDiv.current.removeChild(renderer.domElement);
}
if (landingDiv) {
document.removeEventListener("mousemove", onMouseMove);
landingDiv.removeEventListener("touchstart", onTouchStart);
landingDiv.removeEventListener("touchend", onTouchEnd);
}
};
}
}, []);
return (
<>
<div className="character-container">
<div className="character-model" ref={canvasDiv}>
<div className="character-rim"></div>
<div className="character-hover" ref={hoverDivRef}></div>
</div>
</div>
</>
);
};
export default Scene;

View File

View File

@@ -0,0 +1,7 @@
import Scene from "./Scene";
const CharacterModel = () => {
return <Scene />;
};
export default CharacterModel;

View File

@@ -0,0 +1,118 @@
import * as THREE from "three";
import { GLTF } from "three-stdlib";
import { eyebrowBoneNames, typingBoneNames } from "../../../data/boneData";
const setAnimations = (gltf: GLTF) => {
let character = gltf.scene;
let mixer = new THREE.AnimationMixer(character);
if (gltf.animations) {
const introClip = gltf.animations.find(
(clip) => clip.name === "introAnimation"
);
const introAction = mixer.clipAction(introClip!);
introAction.setLoop(THREE.LoopOnce, 1);
introAction.clampWhenFinished = true;
introAction.play();
const clipNames = ["key1", "key2", "key5", "key6"];
clipNames.forEach((name) => {
const clip = THREE.AnimationClip.findByName(gltf.animations, name);
if (clip) {
const action = mixer?.clipAction(clip);
action!.play();
action!.timeScale = 1.2;
} else {
console.error(`Animation "${name}" not found`);
}
});
let typingAction: THREE.AnimationAction | null = null;
typingAction = createBoneAction(gltf, mixer, "typing", typingBoneNames);
if (typingAction) {
typingAction.enabled = true;
typingAction.play();
typingAction.timeScale = 1.2;
}
}
function startIntro() {
const introClip = gltf.animations.find(
(clip) => clip.name === "introAnimation"
);
const introAction = mixer.clipAction(introClip!);
introAction.clampWhenFinished = true;
introAction.reset().play();
setTimeout(() => {
const blink = gltf.animations.find((clip) => clip.name === "Blink");
mixer.clipAction(blink!).play().fadeIn(0.5);
}, 2500);
}
function hover(gltf: GLTF, hoverDiv: HTMLDivElement) {
let eyeBrowUpAction = createBoneAction(
gltf,
mixer,
"browup",
eyebrowBoneNames
);
let isHovering = false;
if (eyeBrowUpAction) {
eyeBrowUpAction.setLoop(THREE.LoopOnce, 1);
eyeBrowUpAction.clampWhenFinished = true;
eyeBrowUpAction.enabled = true;
}
const onHoverFace = () => {
if (eyeBrowUpAction && !isHovering) {
isHovering = true;
eyeBrowUpAction.reset();
eyeBrowUpAction.enabled = true;
eyeBrowUpAction.setEffectiveWeight(4);
eyeBrowUpAction.fadeIn(0.5).play();
}
};
const onLeaveFace = () => {
if (eyeBrowUpAction && isHovering) {
isHovering = false;
eyeBrowUpAction.fadeOut(0.6);
}
};
if (!hoverDiv) return;
hoverDiv.addEventListener("mouseenter", onHoverFace);
hoverDiv.addEventListener("mouseleave", onLeaveFace);
return () => {
hoverDiv.removeEventListener("mouseenter", onHoverFace);
hoverDiv.removeEventListener("mouseleave", onLeaveFace);
};
}
return { mixer, startIntro, hover };
};
const createBoneAction = (
gltf: GLTF,
mixer: THREE.AnimationMixer,
clip: string,
boneNames: string[]
): THREE.AnimationAction | null => {
const AnimationClip = THREE.AnimationClip.findByName(gltf.animations, clip);
if (!AnimationClip) {
console.error(`Animation "${clip}" not found in GLTF file.`);
return null;
}
const filteredClip = filterAnimationTracks(AnimationClip, boneNames);
return mixer.clipAction(filteredClip);
};
const filterAnimationTracks = (
clip: THREE.AnimationClip,
boneNames: string[]
): THREE.AnimationClip => {
const filteredTracks = clip.tracks.filter((track) =>
boneNames.some((boneName) => track.name.includes(boneName))
);
return new THREE.AnimationClip(
clip.name + "_filtered",
clip.duration,
filteredTracks
);
};
export default setAnimations;

View File

@@ -0,0 +1,62 @@
import * as THREE from "three";
import { DRACOLoader, GLTF, GLTFLoader } from "three-stdlib";
import { setCharTimeline, setAllTimeline } from "../../utils/GsapScroll";
import { decryptFile } from "./decrypt";
const setCharacter = (
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.PerspectiveCamera
) => {
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/");
loader.setDRACOLoader(dracoLoader);
const loadCharacter = () => {
return new Promise<GLTF | null>(async (resolve, reject) => {
try {
const encryptedBlob = await decryptFile(
"/models/character.enc",
"Character3D#@"
);
const blobUrl = URL.createObjectURL(new Blob([encryptedBlob]));
let character: THREE.Object3D;
loader.load(
blobUrl,
async (gltf) => {
character = gltf.scene;
await renderer.compileAsync(character, camera, scene);
character.traverse((child: any) => {
if (child.isMesh) {
const mesh = child as THREE.Mesh;
child.castShadow = true;
child.receiveShadow = true;
mesh.frustumCulled = true;
}
});
resolve(gltf);
setCharTimeline(character, camera);
setAllTimeline();
character!.getObjectByName("footR")!.position.y = 3.36;
character!.getObjectByName("footL")!.position.y = 3.36;
dracoLoader.dispose();
},
undefined,
(error) => {
console.error("Error loading GLTF model:", error);
reject(error);
}
);
} catch (err) {
reject(err);
console.error(err);
}
});
};
return { loadCharacter };
};
export default setCharacter;

View File

@@ -0,0 +1,23 @@
async function generateAESKey(password: string): Promise<CryptoKey> {
const passwordBuffer = new TextEncoder().encode(password);
const hashedPassword = await crypto.subtle.digest("SHA-256", passwordBuffer);
return crypto.subtle.importKey(
"raw",
hashedPassword.slice(0, 32),
{ name: "AES-CBC" },
false,
["encrypt", "decrypt"]
);
}
export const decryptFile = async (
url: string,
password: string
): Promise<ArrayBuffer> => {
const response = await fetch(url);
const encryptedData = await response.arrayBuffer();
const iv = new Uint8Array(encryptedData.slice(0, 16));
const data = encryptedData.slice(16);
const key = await generateAESKey(password);
return crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data);
};

View File

@@ -0,0 +1,61 @@
import * as THREE from "three";
import { RGBELoader } from "three-stdlib";
import { gsap } from "gsap";
const setLighting = (scene: THREE.Scene) => {
const directionalLight = new THREE.DirectionalLight(0xc7a9ff, 0);
directionalLight.intensity = 0;
directionalLight.position.set(-0.47, -0.32, -1);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
scene.add(directionalLight);
const pointLight = new THREE.PointLight(0xc2a4ff, 0, 100, 3);
pointLight.position.set(3, 12, 4);
pointLight.castShadow = true;
scene.add(pointLight);
new RGBELoader()
.setPath("/models/")
.load("char_enviorment.hdr", function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.environmentIntensity = 0;
scene.environmentRotation.set(5.76, 85.85, 1);
});
function setPointLight(screenLight: any) {
if (screenLight.material.opacity > 0.9) {
pointLight.intensity = screenLight.material.emissiveIntensity * 20;
} else {
pointLight.intensity = 0;
}
}
const duration = 2;
const ease = "power2.inOut";
function turnOnLights() {
gsap.to(scene, {
environmentIntensity: 0.64,
duration: duration,
ease: ease,
});
gsap.to(directionalLight, {
intensity: 1,
duration: duration,
ease: ease,
});
gsap.to(".character-rim", {
y: "55%",
opacity: 1,
delay: 0.2,
duration: 2,
});
}
return { setPointLight, turnOnLights };
};
export default setLighting;

View File

@@ -0,0 +1,82 @@
import * as THREE from "three";
export const handleMouseMove = (
event: MouseEvent,
setMousePosition: (x: number, y: number) => void
) => {
const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
const mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
setMousePosition(mouseX, mouseY);
};
export const handleTouchMove = (
event: TouchEvent,
setMousePosition: (x: number, y: number) => void
) => {
const mouseX = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
const mouseY = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
setMousePosition(mouseX, mouseY);
};
export const handleTouchEnd = (
setMousePosition: (
x: number,
y: number,
interpolationX: number,
interpolationY: number
) => void
) => {
setTimeout(() => {
setMousePosition(0, 0, 0.03, 0.03);
setTimeout(() => {
setMousePosition(0, 0, 0.1, 0.2);
}, 1000);
}, 2000);
};
export const handleHeadRotation = (
headBone: THREE.Object3D,
mouseX: number,
mouseY: number,
interpolationX: number,
interpolationY: number,
lerp: (x: number, y: number, t: number) => number
) => {
if (!headBone) return;
if (window.scrollY < 200) {
const maxRotation = Math.PI / 6;
headBone.rotation.y = lerp(
headBone.rotation.y,
mouseX * maxRotation,
interpolationY
);
let minRotationX = -0.3;
let maxRotationX = 0.4;
if (mouseY > minRotationX) {
if (mouseY < maxRotationX) {
headBone.rotation.x = lerp(
headBone.rotation.x,
-mouseY - 0.5 * maxRotation,
interpolationX
);
} else {
headBone.rotation.x = lerp(
headBone.rotation.x,
-maxRotation - 0.5 * maxRotation,
interpolationX
);
}
} else {
headBone.rotation.x = lerp(
headBone.rotation.x,
-minRotationX - 0.5 * maxRotation,
interpolationX
);
}
} else {
if (window.innerWidth > 1024) {
headBone.rotation.x = lerp(headBone.rotation.x, -0.4, 0.03);
headBone.rotation.y = lerp(headBone.rotation.y, -0.3, 0.03);
}
}
};

View File

@@ -0,0 +1,26 @@
import * as THREE from "three";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { setCharTimeline, setAllTimeline } from "../../utils/GsapScroll";
export default function handleResize(
renderer: THREE.WebGLRenderer,
camera: THREE.PerspectiveCamera,
canvasDiv: React.RefObject<HTMLDivElement>,
character: THREE.Object3D
) {
if (!canvasDiv.current) return;
let canvas3d = canvasDiv.current.getBoundingClientRect();
const width = canvas3d.width;
const height = canvas3d.height;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
const workTrigger = ScrollTrigger.getById("work");
ScrollTrigger.getAll().forEach((trigger) => {
if (trigger != workTrigger) {
trigger.kill();
}
});
setCharTimeline(character, camera);
setAllTimeline();
}

View File

@@ -0,0 +1,65 @@
import { MdArrowOutward, MdCopyright } from "react-icons/md";
import "./styles/Contact.css";
const Contact = () => {
return (
<div className="contact-section section-container" id="contact">
<div className="contact-container">
<h3>Contact</h3>
<div className="contact-flex">
<div className="contact-box">
<h4>Email</h4>
<p>
<a href="mailto:phongpt.working@gmail.com" data-cursor="disable">
phongpt.working@gmail.com
</a>
</p>
<h4>Phone</h4>
<p>
<a href="tel:+84867524445" data-cursor="disable">
+84 86 752 4445
</a>
</p>
</div>
<div className="contact-box">
<h4>Social</h4>
<a
href="https://gitea.phongprojects.id.vn/phongpham"
target="_blank"
data-cursor="disable"
className="contact-social"
>
Gitea <MdArrowOutward />
</a>
<a
href="https://www.linkedin.com/in/phong-pham"
target="_blank"
data-cursor="disable"
className="contact-social"
>
Linkedin <MdArrowOutward />
</a>
<a
href="https://github.com/phongpham"
target="_blank"
data-cursor="disable"
className="contact-social"
>
Github <MdArrowOutward />
</a>
</div>
<div className="contact-box">
<h2>
Designed and Developed <br /> by <span>Phong Pham</span>
</h2>
<h5>
<MdCopyright /> 2025
</h5>
</div>
</div>
</div>
</div>
);
};
export default Contact;

54
src/components/Cursor.tsx Normal file
View File

@@ -0,0 +1,54 @@
import { useEffect, useRef } from "react";
import "./styles/Cursor.css";
import gsap from "gsap";
const Cursor = () => {
const cursorRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let hover = false;
const cursor = cursorRef.current!;
const mousePos = { x: 0, y: 0 };
const cursorPos = { x: 0, y: 0 };
document.addEventListener("mousemove", (e) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});
requestAnimationFrame(function loop() {
if (!hover) {
const delay = 6;
cursorPos.x += (mousePos.x - cursorPos.x) / delay;
cursorPos.y += (mousePos.y - cursorPos.y) / delay;
gsap.to(cursor, { x: cursorPos.x, y: cursorPos.y, duration: 0.1 });
// cursor.style.transform = `translate(${cursorPos.x}px, ${cursorPos.y}px)`;
}
requestAnimationFrame(loop);
});
document.querySelectorAll("[data-cursor]").forEach((item) => {
const element = item as HTMLElement;
element.addEventListener("mouseover", (e: MouseEvent) => {
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
if (element.dataset.cursor === "icons") {
cursor.classList.add("cursor-icons");
gsap.to(cursor, { x: rect.left, y: rect.top, duration: 0.1 });
// cursor.style.transform = `translate(${rect.left}px,${rect.top}px)`;
cursor.style.setProperty("--cursorH", `${rect.height}px`);
hover = true;
}
if (element.dataset.cursor === "disable") {
cursor.classList.add("cursor-disable");
}
});
element.addEventListener("mouseout", () => {
cursor.classList.remove("cursor-disable", "cursor-icons");
hover = false;
});
});
}, []);
return <div className="cursor-main" ref={cursorRef}></div>;
};
export default Cursor;

View File

@@ -0,0 +1,13 @@
import "./styles/style.css";
const HoverLinks = ({ text, cursor }: { text: string; cursor?: boolean }) => {
return (
<div className="hover-link" data-cursor={!cursor && `disable`}>
<div className="hover-in">
{text} <div>{text}</div>
</div>
</div>
);
};
export default HoverLinks;

View File

@@ -0,0 +1,35 @@
import { PropsWithChildren } from "react";
import "./styles/Landing.css";
const Landing = ({ children }: PropsWithChildren) => {
return (
<>
<div className="landing-section" id="landingDiv">
<div className="landing-container">
<div className="landing-intro">
<h2>Hello! I'm</h2>
<h1>
PHONG
<br />
<span>PHAM</span>
</h1>
</div>
<div className="landing-info">
<h3>QA & Automation</h3>
<h2 className="landing-info-h2">
<div className="landing-h2-1">Engineer</div>
<div className="landing-h2-2">Tester</div>
</h2>
<h2>
<div className="landing-h2-info">Tester</div>
<div className="landing-h2-info-1">Engineer</div>
</h2>
</div>
</div>
{children}
</div>
</>
);
};
export default Landing;

135
src/components/Loading.tsx Normal file
View File

@@ -0,0 +1,135 @@
import { useEffect, useState } from "react";
import "./styles/Loading.css";
import { useLoading } from "../context/LoadingProvider";
import Marquee from "react-fast-marquee";
const Loading = ({ percent }: { percent: number }) => {
const { setIsLoading } = useLoading();
const [loaded, setLoaded] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
if (percent >= 100) {
setTimeout(() => {
setLoaded(true);
setTimeout(() => {
setIsLoaded(true);
}, 1000);
}, 600);
}
useEffect(() => {
import("./utils/initialFX").then((module) => {
if (isLoaded) {
setClicked(true);
setTimeout(() => {
if (module.initialFX) {
module.initialFX();
}
setIsLoading(false);
}, 900);
}
});
}, [isLoaded]);
function handleMouseMove(e: React.MouseEvent<HTMLElement>) {
const { currentTarget: target } = e;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
target.style.setProperty("--mouse-x", `${x}px`);
target.style.setProperty("--mouse-y", `${y}px`);
}
return (
<>
<div className="loading-header">
<a href="/#" className="loader-title" data-cursor="disable">
Logo
</a>
<div className={`loaderGame ${clicked && "loader-out"}`}>
<div className="loaderGame-container">
<div className="loaderGame-in">
{[...Array(27)].map((_, index) => (
<div className="loaderGame-line" key={index}></div>
))}
</div>
<div className="loaderGame-ball"></div>
</div>
</div>
</div>
<div className="loading-screen">
<div className="loading-marquee">
<Marquee>
<span> A Creative Developer</span> <span>A Creative Designer</span>
<span> A Creative Developer</span> <span>A Creative Designer</span>
</Marquee>
</div>
<div
className={`loading-wrap ${clicked && "loading-clicked"}`}
onMouseMove={(e) => handleMouseMove(e)}
>
<div className="loading-hover"></div>
<div className={`loading-button ${loaded && "loading-complete"}`}>
<div className="loading-container">
<div className="loading-content">
<div className="loading-content-in">
Loading <span>{percent}%</span>
</div>
</div>
<div className="loading-box"></div>
</div>
<div className="loading-content2">
<span>Welcome</span>
</div>
</div>
</div>
</div>
</>
);
};
export default Loading;
export const setProgress = (setLoading: (value: number) => void) => {
let percent: number = 0;
let interval = setInterval(() => {
if (percent <= 50) {
let rand = Math.round(Math.random() * 5);
percent = percent + rand;
setLoading(percent);
} else {
clearInterval(interval);
interval = setInterval(() => {
percent = percent + Math.round(Math.random());
setLoading(percent);
if (percent > 91) {
clearInterval(interval);
}
}, 2000);
}
}, 100);
function clear() {
clearInterval(interval);
setLoading(100);
}
function loaded() {
return new Promise<number>((resolve) => {
clearInterval(interval);
interval = setInterval(() => {
if (percent < 100) {
percent++;
setLoading(percent);
} else {
resolve(percent);
clearInterval(interval);
}
}, 2);
});
}
return { loaded, percent, clear };
};

View File

@@ -0,0 +1,59 @@
import { lazy, PropsWithChildren, Suspense, useEffect, useState } from "react";
import About from "./About";
import Career from "./Career";
import Contact from "./Contact";
import Cursor from "./Cursor";
import Landing from "./Landing";
import Navbar from "./Navbar";
import SocialIcons from "./SocialIcons";
import WhatIDo from "./WhatIDo";
import Work from "./Work";
import setSplitText from "./utils/splitText";
const TechStack = lazy(() => import("./TechStack"));
const MainContainer = ({ children }: PropsWithChildren) => {
const [isDesktopView, setIsDesktopView] = useState<boolean>(
window.innerWidth > 1024
);
useEffect(() => {
const resizeHandler = () => {
setSplitText();
setIsDesktopView(window.innerWidth > 1024);
};
resizeHandler();
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
};
}, [isDesktopView]);
return (
<div className="container-main">
<Cursor />
<Navbar />
<SocialIcons />
{isDesktopView && children}
<div id="smooth-wrapper">
<div id="smooth-content">
<div className="container-main">
<Landing>{!isDesktopView && children}</Landing>
<About />
<WhatIDo />
<Career />
<Work />
{isDesktopView && (
<Suspense fallback={<div>Loading....</div>}>
<TechStack />
</Suspense>
)}
<Contact />
</div>
</div>
</div>
</div>
);
};
export default MainContainer;

81
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,81 @@
import { useEffect } from "react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import HoverLinks from "./HoverLinks";
import { gsap } from "gsap";
import { ScrollSmoother } from "gsap/dist/ScrollSmoother";
import "./styles/Navbar.css";
gsap.registerPlugin(ScrollSmoother, ScrollTrigger);
export let smoother: ScrollSmoother;
const Navbar = () => {
useEffect(() => {
smoother = ScrollSmoother.create({
wrapper: "#smooth-wrapper",
content: "#smooth-content",
smooth: 1.7,
speed: 1.7,
effects: true,
autoResize: true,
ignoreMobileResize: true,
});
smoother.scrollTop(0);
smoother.paused(true);
let links = document.querySelectorAll(".header ul a");
links.forEach((elem) => {
let element = elem as HTMLAnchorElement;
element.addEventListener("click", (e) => {
if (window.innerWidth > 1024) {
e.preventDefault();
let elem = e.currentTarget as HTMLAnchorElement;
let section = elem.getAttribute("data-href");
smoother.scrollTo(section, true, "top top");
}
});
});
window.addEventListener("resize", () => {
ScrollSmoother.refresh(true);
});
}, []);
return (
<>
<div className="header">
<a href="/#" className="navbar-title" data-cursor="disable">
PhongPham.DEV
</a>
<a
href="mailto:phongpt.working@gmail.com"
className="navbar-connect"
data-cursor="disable"
>
phongpt.working@gmail.com
</a>
<ul>
<li>
<a data-href="#about" href="#about">
<HoverLinks text="ABOUT" />
</a>
</li>
<li>
<a data-href="#work" href="#work">
<HoverLinks text="WORK" />
</a>
</li>
<li>
<a data-href="#contact" href="#contact">
<HoverLinks text="CONTACT" />
</a>
</li>
</ul>
</div>
<div className="landing-circle1"></div>
<div className="landing-circle2"></div>
<div className="nav-fade"></div>
</>
);
};
export default Navbar;

View File

@@ -0,0 +1,93 @@
import {
FaGithub,
FaLinkedinIn,
FaFacebookF,
} from "react-icons/fa6";
import { SiGitea } from "react-icons/si";
import "./styles/SocialIcons.css";
import { TbNotes } from "react-icons/tb";
import { useEffect } from "react";
import HoverLinks from "./HoverLinks";
const SocialIcons = () => {
useEffect(() => {
const social = document.getElementById("social") as HTMLElement;
social.querySelectorAll("span").forEach((item) => {
const elem = item as HTMLElement;
const link = elem.querySelector("a") as HTMLElement;
const rect = elem.getBoundingClientRect();
let mouseX = rect.width / 2;
let mouseY = rect.height / 2;
let currentX = 0;
let currentY = 0;
const updatePosition = () => {
currentX += (mouseX - currentX) * 0.1;
currentY += (mouseY - currentY) * 0.1;
link.style.setProperty("--siLeft", `${currentX}px`);
link.style.setProperty("--siTop", `${currentY}px`);
requestAnimationFrame(updatePosition);
};
const onMouseMove = (e: MouseEvent) => {
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (x < 40 && x > 10 && y < 40 && y > 5) {
mouseX = x;
mouseY = y;
} else {
mouseX = rect.width / 2;
mouseY = rect.height / 2;
}
};
document.addEventListener("mousemove", onMouseMove);
updatePosition();
return () => {
elem.removeEventListener("mousemove", onMouseMove);
};
});
}, []);
return (
<div className="icons-section">
<div className="social-icons" data-cursor="icons" id="social">
<span>
<a href="https://github.com/Phong12HexDockwork" target="_blank">
<FaGithub />
</a>
</span>
<span>
<a href="https://gitea.phongprojects.id.vn/phongpham" target="_blank">
<SiGitea />
</a>
</span>
<span>
<a href="https://www.facebook.com" target="_blank">
<FaFacebookF />
</a>
</span>
<span>
<a href="https://www.linkedin.com/in/phong-pham" target="_blank">
<FaLinkedinIn />
</a>
</span>
</div>
<a className="resume-button" href="https://docs.google.com/document/d/1JizajaaBrOGiEnE7mvA_CrjxwyWW82oJtfToM5QpTaI/edit?usp=sharing" target="_blank" rel="noopener noreferrer">
<HoverLinks text="RESUME" />
<span>
<TbNotes />
</span>
</a>
</div>
);
};
export default SocialIcons;

View File

@@ -0,0 +1,214 @@
import * as THREE from "three";
import { useRef, useMemo, useState, useEffect } from "react";
import { Canvas, useFrame } from "@react-three/fiber";
import { Environment } from "@react-three/drei";
import { EffectComposer, N8AO } from "@react-three/postprocessing";
import {
BallCollider,
Physics,
RigidBody,
CylinderCollider,
RapierRigidBody,
} from "@react-three/rapier";
const textureLoader = new THREE.TextureLoader();
const imageUrls = [
"/images/react2.webp",
"/images/next2.webp",
"/images/node2.webp",
"/images/express.webp",
"/images/mongo.webp",
"/images/mysql.webp",
"/images/typescript.webp",
"/images/javascript.webp",
];
const textures = imageUrls.map((url) => textureLoader.load(url));
const sphereGeometry = new THREE.SphereGeometry(1, 28, 28);
const spheres = [...Array(30)].map(() => ({
scale: [0.7, 1, 0.8, 1, 1][Math.floor(Math.random() * 5)],
}));
type SphereProps = {
vec?: THREE.Vector3;
scale: number;
r?: typeof THREE.MathUtils.randFloatSpread;
material: THREE.MeshPhysicalMaterial;
isActive: boolean;
};
function SphereGeo({
vec = new THREE.Vector3(),
scale,
r = THREE.MathUtils.randFloatSpread,
material,
isActive,
}: SphereProps) {
const api = useRef<RapierRigidBody | null>(null);
useFrame((_state, delta) => {
if (!isActive) return;
delta = Math.min(0.1, delta);
const impulse = vec
.copy(api.current!.translation())
.normalize()
.multiply(
new THREE.Vector3(
-50 * delta * scale,
-150 * delta * scale,
-50 * delta * scale
)
);
api.current?.applyImpulse(impulse, true);
});
return (
<RigidBody
linearDamping={0.75}
angularDamping={0.15}
friction={0.2}
position={[r(20), r(20) - 25, r(20) - 10]}
ref={api}
colliders={false}
>
<BallCollider args={[scale]} />
<CylinderCollider
rotation={[Math.PI / 2, 0, 0]}
position={[0, 0, 1.2 * scale]}
args={[0.15 * scale, 0.275 * scale]}
/>
<mesh
castShadow
receiveShadow
scale={scale}
geometry={sphereGeometry}
material={material}
rotation={[0.3, 1, 1]}
/>
</RigidBody>
);
}
type PointerProps = {
vec?: THREE.Vector3;
isActive: boolean;
};
function Pointer({ vec = new THREE.Vector3(), isActive }: PointerProps) {
const ref = useRef<RapierRigidBody>(null);
useFrame(({ pointer, viewport }) => {
if (!isActive) return;
const targetVec = vec.lerp(
new THREE.Vector3(
(pointer.x * viewport.width) / 2,
(pointer.y * viewport.height) / 2,
0
),
0.2
);
ref.current?.setNextKinematicTranslation(targetVec);
});
return (
<RigidBody
position={[100, 100, 100]}
type="kinematicPosition"
colliders={false}
ref={ref}
>
<BallCollider args={[2]} />
</RigidBody>
);
}
const TechStack = () => {
const [isActive, setIsActive] = useState(false);
useEffect(() => {
const handleScroll = () => {
const scrollY = window.scrollY || document.documentElement.scrollTop;
const threshold = document
.getElementById("work")!
.getBoundingClientRect().top;
setIsActive(scrollY > threshold);
};
document.querySelectorAll(".header a").forEach((elem) => {
const element = elem as HTMLAnchorElement;
element.addEventListener("click", () => {
const interval = setInterval(() => {
handleScroll();
}, 10);
setTimeout(() => {
clearInterval(interval);
}, 1000);
});
});
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const materials = useMemo(() => {
return textures.map(
(texture) =>
new THREE.MeshPhysicalMaterial({
map: texture,
emissive: "#ffffff",
emissiveMap: texture,
emissiveIntensity: 0.3,
metalness: 0.5,
roughness: 1,
clearcoat: 0.1,
})
);
}, []);
return (
<div className="techstack">
<h2> My Techstack</h2>
<Canvas
shadows
gl={{ alpha: true, stencil: false, depth: false, antialias: false }}
camera={{ position: [0, 0, 20], fov: 32.5, near: 1, far: 100 }}
onCreated={(state) => (state.gl.toneMappingExposure = 1.5)}
className="tech-canvas"
>
<ambientLight intensity={1} />
<spotLight
position={[20, 20, 25]}
penumbra={1}
angle={0.2}
color="white"
castShadow
shadow-mapSize={[512, 512]}
/>
<directionalLight position={[0, 5, -4]} intensity={2} />
<Physics gravity={[0, 0, 0]}>
<Pointer isActive={isActive} />
{spheres.map((props, i) => (
<SphereGeo
key={i}
{...props}
material={materials[Math.floor(Math.random() * materials.length)]}
isActive={isActive}
/>
))}
</Physics>
<Environment
files="/models/char_enviorment.hdr"
environmentIntensity={0.5}
environmentRotation={[0, 4, 2]}
/>
<EffectComposer enableNormalPass={false}>
<N8AO color="#0f002c" aoRadius={2} intensity={1.15} />
</EffectComposer>
</Canvas>
</div>
);
};
export default TechStack;

172
src/components/WhatIDo.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { useEffect, useRef } from "react";
import "./styles/WhatIDo.css";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const WhatIDo = () => {
const containerRef = useRef<(HTMLDivElement | null)[]>([]);
const setRef = (el: HTMLDivElement | null, index: number) => {
containerRef.current[index] = el;
};
useEffect(() => {
if (ScrollTrigger.isTouch) {
containerRef.current.forEach((container) => {
if (container) {
container.classList.remove("what-noTouch");
container.addEventListener("click", () => handleClick(container));
}
});
}
return () => {
containerRef.current.forEach((container) => {
if (container) {
container.removeEventListener("click", () => handleClick(container));
}
});
};
}, []);
return (
<div className="whatIDO">
<div className="what-box">
<h2 className="title">
W<span className="hat-h2">HAT</span>
<div>
I<span className="do-h2"> DO</span>
</div>
</h2>
</div>
<div className="what-box">
<div className="what-box-in">
<div className="what-border2">
<svg width="100%">
<line
x1="0"
y1="0"
x2="0"
y2="100%"
stroke="white"
strokeWidth="2"
strokeDasharray="7,7"
/>
<line
x1="100%"
y1="0"
x2="100%"
y2="100%"
stroke="white"
strokeWidth="2"
strokeDasharray="7,7"
/>
</svg>
</div>
<div
className="what-content what-noTouch"
ref={(el) => setRef(el, 0)}
>
<div className="what-border1">
<svg height="100%">
<line
x1="0"
y1="0"
x2="100%"
y2="0"
stroke="white"
strokeWidth="2"
strokeDasharray="6,6"
/>
<line
x1="0"
y1="100%"
x2="100%"
y2="100%"
stroke="white"
strokeWidth="2"
strokeDasharray="6,6"
/>
</svg>
</div>
<div className="what-corner"></div>
<div className="what-content-in">
<h3>QA TESTING</h3>
<h4>Description</h4>
<p>
Manual, API and database testing to ensure system stability and data integrity.
Designing test cases, tracking bugs, and performing regression testing across platforms.
</p>
<h5>Skillset & tools</h5>
<div className="what-content-flex">
<div className="what-tags">Jira</div>
<div className="what-tags">TestRail</div>
<div className="what-tags">Postman</div>
<div className="what-tags">Swagger</div>
<div className="what-tags">SQL</div>
<div className="what-tags">API Testing</div>
<div className="what-tags">Regression Testing</div>
<div className="what-tags">Chrome DevTools</div>
<div className="what-tags">Bug Tracking</div>
<div className="what-tags">Agile</div>
</div>
<div className="what-arrow"></div>
</div>
</div>
<div
className="what-content what-noTouch"
ref={(el) => setRef(el, 1)}
>
<div className="what-border1">
<svg height="100%">
<line
x1="0"
y1="100%"
x2="100%"
y2="100%"
stroke="white"
strokeWidth="2"
strokeDasharray="6,6"
/>
</svg>
</div>
<div className="what-corner"></div>
<div className="what-content-in">
<h3>AUTOMATION</h3>
<h4>Description</h4>
<p>
Workflow automation and test efficiency improvement using modern tools.
Building automated processes for testing, data management, and system integration.
</p>
<h5>Skillset & tools</h5>
<div className="what-content-flex">
<div className="what-tags">n8n</div>
<div className="what-tags">JavaScript</div>
<div className="what-tags">Python</div>
<div className="what-tags">Google App Scripts</div>
<div className="what-tags">Git</div>
<div className="what-tags">Workflow Automation</div>
<div className="what-tags">CI/CD</div>
<div className="what-tags">Test Frameworks</div>
</div>
<div className="what-arrow"></div>
</div>
</div>
</div>
</div>
</div>
);
};
export default WhatIDo;
function handleClick(container: HTMLDivElement) {
container.classList.toggle("what-content-active");
container.classList.remove("what-sibling");
if (container.parentElement) {
const siblings = Array.from(container.parentElement.children);
siblings.forEach((sibling) => {
if (sibling !== container) {
sibling.classList.remove("what-content-active");
sibling.classList.toggle("what-sibling");
}
});
}
}

128
src/components/Work.tsx Normal file
View File

@@ -0,0 +1,128 @@
import "./styles/Work.css";
import WorkImage from "./WorkImage";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useEffect } from "react";
gsap.registerPlugin(ScrollTrigger);
const Work = () => {
useEffect(() => {
let translateX: number = 0;
function setTranslateX() {
const box = document.getElementsByClassName("work-box");
const rectLeft = document
.querySelector(".work-container")!
.getBoundingClientRect().left;
const rect = box[0].getBoundingClientRect();
const parentWidth = box[0].parentElement!.getBoundingClientRect().width;
let padding: number =
parseInt(window.getComputedStyle(box[0]).padding) / 2;
translateX = rect.width * box.length - (rectLeft + parentWidth) + padding;
}
setTranslateX();
let timeline = gsap.timeline({
scrollTrigger: {
trigger: ".work-section",
start: "top top",
end: `+=${translateX}`, // Use actual scroll width
scrub: true,
pin: true,
id: "work",
},
});
timeline.to(".work-flex", {
x: -translateX,
ease: "none",
});
// Clean up (optional, good practice)
return () => {
timeline.kill();
ScrollTrigger.getById("work")?.kill();
};
}, []);
const projects = [
{
title: "UVita - UV Application",
category: "QA & Development",
description: "Led QC activities and data integrity testing for real-time UV-index app. Managed bug life cycle within 48-hour hackathon deadline.",
tools: "Postman, TestRail, Git, Agile Workflow",
image: "/project_img/UVIita.png",
link: "https://github.com/Phong12HexDockwork/UVita"
},
{
title: "LACA City Platform",
category: "Software Testing",
description: "API and database testing for parking management platform serving 5,000+ users. Improved release stability by 20%.",
tools: "Jira, Postman, Chrome DevTools, SQL",
image: "/project_img/Laca City.png",
link: "https://new.laca.city"
},
{
title: "NASA Space Apps Challenge",
category: "Quality Control & Operations",
description: "Designed QC processes for event with 500+ participants. Increased participation by 126% compared to previous year.",
tools: "n8n, TestRail, Meta Console, API Testing",
image: "/project_img/NASA Space app.jpg",
link: "https://www.facebook.com/spaceappshochiminh"
},
{
title: "VSIG Platform",
category: "Testing & Automation",
description: "Executed 150+ test cases achieving >98% release stability. Reduced issue resolution time by 25%.",
tools: "TestRail, Postman, Swagger, Jira, n8n",
image: "/project_img/VSIG .png",
link: ""
},
{
title: "CSED Conference 2024",
category: "Development & Automation",
description: "Developed automated submission system reducing processing time by 40%. Supported 200+ submissions with workflow automation.",
tools: "Google App Scripts, n8n, JavaScript",
image: "/project_img/CSED.jpg",
link: ""
}
];
return (
<div className="work-section" id="work">
<div className="work-container section-container">
<h2>
My <span>Work</span>
</h2>
<div className="work-flex">
{projects.map((project, index) => (
<div className="work-box" key={index}>
<div className="work-info">
<div className="work-title">
<h3>0{index + 1}</h3>
<div>
<h4>{project.title}</h4>
<p>{project.category}</p>
</div>
</div>
<h4>{project.description}</h4>
<p>{project.tools}</p>
</div>
{project.link ? (
<a href={project.link} target="_blank" rel="noopener noreferrer">
<WorkImage image={project.image} alt={project.title} />
</a>
) : (
<WorkImage image={project.image} alt={project.title} />
)}
</div>
))}
</div>
</div>
</div>
);
};
export default Work;

View File

@@ -0,0 +1,46 @@
import { useState } from "react";
import { MdArrowOutward } from "react-icons/md";
interface Props {
image: string;
alt?: string;
video?: string;
link?: string;
}
const WorkImage = (props: Props) => {
const [isVideo, setIsVideo] = useState(false);
const [video, setVideo] = useState("");
const handleMouseEnter = async () => {
if (props.video) {
setIsVideo(true);
const response = await fetch(`src/assets/${props.video}`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
setVideo(blobUrl);
}
};
return (
<div className="work-image">
<a
className="work-image-in"
href={props.link}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setIsVideo(false)}
target="_blank"
data-cursor={"disable"}
>
{props.link && (
<div className="work-link">
<MdArrowOutward />
</div>
)}
<img src={props.image} alt={props.alt} />
{isVideo && <video src={video} autoPlay muted playsInline loop></video>}
</a>
</div>
);
};
export default WorkImage;

View File

@@ -0,0 +1,71 @@
.about-section {
display: flex;
align-items: center;
justify-content: left;
place-items: center;
position: relative;
opacity: 1;
height: auto;
width: var(--cWidth);
margin: auto;
}
.about-me {
padding: 50px 0px;
padding-bottom: 0;
width: 500px;
max-width: calc(100% - 15px);
}
.about-me h3 {
font-size: 25px;
text-transform: uppercase;
letter-spacing: 7px;
font-weight: 400;
color: var(--accentColor);
}
.about-me p {
font-size: 20px;
font-weight: 400;
line-height: 28px;
letter-spacing: 0.5px;
}
@media only screen and (min-width: 600px) {
.about-section {
justify-content: center;
}
}
@media only screen and (min-width: 768px) {
.about-me {
width: 500px;
max-width: calc(100% - 70px);
transform: translateY(0%);
}
.about-section {
opacity: 1;
}
}
@media only screen and (min-width: 1025px) {
.about-section {
width: var(--cWidth);
justify-content: right;
max-width: 1920px;
height: var(--vh);
padding: 0px;
opacity: 1;
}
.about-me {
padding: 0px;
width: 50%;
}
.about-me p {
font-size: 1.2vw;
line-height: 1.8vw;
}
}
@media only screen and (min-width: 1950px) {
.about-me p {
font-size: 1.5rem;
line-height: 2rem;
}
}

View File

@@ -0,0 +1,216 @@
.career-section {
display: flex;
flex-direction: column;
align-items: center;
place-items: center;
justify-content: center;
position: relative;
opacity: 1;
height: auto;
margin: auto;
margin-bottom: 250px;
padding: 120px 0px;
}
.career-section h2 {
font-size: 70px;
line-height: 70px;
font-weight: 400;
text-align: center;
background: linear-gradient(0deg, #7f40ff, #ffffff);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
margin-top: 50px;
margin-bottom: 90px;
}
.career-section h2 > span {
font-family: "Geist", sans-serif;
font-weight: 300;
}
.career-info {
position: relative;
display: flex;
flex-direction: column;
margin: 0px auto;
}
.career-info-box {
display: flex;
justify-content: space-between;
margin-bottom: 50px;
}
.career-info-box p {
width: 40%;
font-size: 18px;
font-weight: 300;
margin: 0;
}
.career-info-in {
display: flex;
width: 40%;
justify-content: space-between;
gap: 50px;
}
.career-info h3 {
font-size: 48px;
margin: 0;
font-weight: 500;
line-height: 45px;
}
.career-info h4 {
font-size: 33px;
line-height: 30px;
letter-spacing: 0.8px;
font-weight: 500;
margin: 0;
}
.career-info h5 {
font-weight: 400;
letter-spacing: 0.7px;
font-size: 20px;
text-transform: capitalize;
margin: 10px 0px;
color: var(--accentColor);
}
.career-timeline {
position: absolute;
top: -50px;
left: 50%;
transform: translateX(-50%);
width: 3px;
height: 100%;
background-image: linear-gradient(
to top,
#aa42ff 20%,
var(--accentColor) 50%,
transparent 95%
);
max-height: 0%;
}
.career-dot {
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%, 50%);
background-color: #aa42ff;
width: 10px;
height: 10px;
border-radius: 50px;
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 8px #d097ff,
0px 0px 110px 20px #f2c0ff;
animation: timeline 0.8s linear infinite forwards;
}
@keyframes timeline {
10%,
20%,
50%,
70%,
90% {
box-shadow: 0px 0px 5px 2px #d29bff;
}
10%,
30%,
0%,
100%,
64%,
80% {
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 5px #d097ff,
0px 0px 110px 20px #f2c0ff;
}
}
@keyframes timeline2 {
0% {
box-shadow: 0px 0px 5px 2px #d29bff;
}
100% {
box-shadow: 0px 0px 5px 2px #d29bff, 0px 0px 15px 5px #d097ff,
0px 0px 110px 20px #f2c0ff;
}
}
@media only screen and (max-width: 1400px) {
.career-section h2 {
font-size: 50px;
line-height: 50px;
}
.career-info h4 {
font-size: 22px;
line-height: 24px;
width: 180px;
}
.career-info h5 {
font-size: 17px;
}
.career-info h3 {
font-size: 40px;
}
.career-info-box p {
font-size: 14px;
}
.career-info-in {
width: 45%;
gap: 20px;
}
.career-info-box p {
width: 45%;
}
}
@media only screen and (max-width: 1025px) {
.career-section {
padding: 70px 0px;
padding-top: 220px;
margin-top: -200px;
margin-bottom: 0;
}
}
@media only screen and (max-width: 900px) {
.career-info-box {
flex-direction: column;
gap: 10px;
margin-bottom: 70px;
}
.career-info-in,
.career-info-box p {
width: 100%;
padding-left: 10%;
box-sizing: border-box;
}
.career-timeline {
left: 0%;
}
.career-container {
width: calc(100% - 25px);
}
}
@media only screen and (max-width: 600px) {
.career-info {
margin: 0;
}
.career-section h2 {
width: 100%;
font-size: 45px;
line-height: 45px;
margin-top: 0px;
}
.career-info-in {
gap: 0px;
}
.career-info h3 {
font-size: 33px;
}
.career-info-in,
.career-info-box p {
padding-left: 5%;
}
.career-section {
padding-top: 90px;
margin-top: -70px;
align-items: start;
place-items: inherit;
justify-content: left;
}
}

View File

@@ -0,0 +1,92 @@
.contact-section {
margin: auto;
padding-bottom: 100px;
margin-top: 100px;
}
.contact-section h3 {
font-size: 60px;
font-weight: 400;
text-transform: uppercase;
margin: 0;
}
.contact-flex {
display: flex;
justify-content: space-between;
}
.contact-flex h4 {
font-weight: 500;
margin: 0;
opacity: 0.6;
}
.contact-box {
display: flex;
flex-direction: column;
}
.contact-flex p {
margin-top: 10px;
margin-bottom: 20px;
}
a.contact-social {
font-size: 25px;
border-bottom: 1px solid #ccc;
}
.contact-box h2 {
font-weight: 400;
font-size: 23px;
margin: 0;
}
.contact-box h2 > span {
color: var(--accentColor);
}
.contact-box h5 {
font-size: 20px;
font-weight: 500;
line-height: 20px;
display: flex;
gap: 10px;
opacity: 0.5;
}
@media only screen and (max-width: 1600px) {
.contact-section h3 {
font-size: 50px;
}
.contact-box h2 {
font-size: 20px;
}
a.contact-social {
font-size: 22px;
}
}
@media only screen and (max-width: 1300px) {
.contact-section h3 {
font-size: 40px;
}
.contact-box h2 {
font-size: 18px;
}
a.contact-social {
font-size: 20px;
}
.contact-flex p {
margin-top: 0px;
}
}
@media only screen and (max-width: 900px) {
.contact-flex {
flex-direction: column;
gap: 40px;
}
.contact-flex p {
margin-bottom: 0px;
}
.contact-flex h4 {
margin-top: 20px;
}
.contact-section {
margin-top: 50px;
padding-bottom: 50px;
}
.contact-container {
width: calc(100% - 25px);
}
}

View File

@@ -0,0 +1,33 @@
.cursor-main {
--size: 0px;
position: fixed;
top: calc(var(--size) / -2);
left: calc(var(--size) / -2);
width: var(--size);
height: var(--size);
border-radius: 50px;
pointer-events: none;
z-index: 99;
background-color: #e6c3ff;
box-shadow: 0px 0px 30px 0px rgb(175, 131, 255);
mix-blend-mode: difference;
transition: top 0.3s ease-out, left 0.3s ease-out, width 0.3s ease-out,
height 0.3s ease-out;
}
.cursor-icons {
top: 10px;
left: 10px;
height: calc(var(--cursorH) - 20px);
transition: all 0.5s ease-out, height 0.5s ease-in-out;
}
.cursor-disable {
--size: 0px;
}
@media only screen and (min-width: 600px) {
.cursor-main {
--size: 50px;
}
.cursor-disable {
--size: 0px;
}
}

View File

@@ -0,0 +1,358 @@
.landing-section {
width: 100%;
max-width: var(--cMaxWidth);
margin: auto;
position: relative;
height: var(--vh);
}
.landing-container {
width: var(--cWidth);
margin: auto;
height: 100%;
position: relative;
max-width: var(--cMaxWidth);
}
.landing-circle1 {
top: 0%;
left: 0%;
z-index: 15;
position: fixed;
width: 300px;
height: 300px;
background-color: #fb8dff;
box-shadow: inset -50px 40px 50px rgba(84, 0, 255, 0.6);
filter: blur(60px);
border-radius: 50%;
animation: loadingCircle 5s linear infinite;
}
.nav-fade {
position: fixed;
top: 0;
width: 100%;
height: 130px;
background-image: linear-gradient(
0deg,
transparent,
var(--backgroundColor) 70%
);
pointer-events: none;
z-index: 12;
opacity: 0;
left: 0;
}
@keyframes loadingCircle {
0% {
transform: translate(-95%, -75%) rotateZ(0deg);
}
100% {
transform: translate(-95%, -75%) rotateZ(360deg);
}
}
.landing-circle2 {
top: 50%;
right: 0%;
transform: translate(calc(100% - 2px), -50%);
z-index: 9;
position: fixed;
display: none;
width: 300px;
height: 300px;
background-color: #fb8dff;
box-shadow: inset -50px 40px 50px rgba(84, 0, 255, 0.6);
filter: blur(50px);
border-radius: 50%;
animation: loadingCircle2 5s linear infinite;
}
@keyframes loadingCircle2 {
100% {
transform: translate(calc(100% - 2px), -50%) rotate(360deg);
}
}
.landing-video,
.landing-image {
position: absolute;
bottom: 0;
height: 95%;
left: 50%;
transform: translateX(-50%);
}
.landing-image img {
height: 100%;
z-index: 2;
position: relative;
}
.character-rim {
position: absolute;
width: 400px;
height: 400px;
z-index: 1;
background-color: #f59bf8;
transform: translate(-50%, 36%) scaleX(1.4);
box-shadow: inset 66px 35px 85px 0px rgba(85, 0, 255, 0.65);
filter: blur(50px);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, 100%) scale(1.4);
opacity: 0;
}
.character-model {
height: 80%;
height: 80vh;
position: absolute;
max-width: 1920px;
max-height: 1080px;
transform: translateX(-50%);
width: 100%;
left: 50%;
z-index: 0;
bottom: 50px;
pointer-events: inherit;
}
.character-model::after {
content: "";
width: 100vw;
height: 250px;
background-image: linear-gradient(
to bottom,
transparent,
var(--backgroundColor) 70%
);
bottom: -50px;
left: 50%;
transform: translateX(-50%);
z-index: 9;
position: absolute;
}
.character-model::before {
content: "";
width: 100vw;
height: 700px;
background-color: var(--backgroundColor);
top: 100%;
left: 50%;
transform: translateX(-50%);
z-index: 9;
position: absolute;
}
.character-loaded .character-rim {
animation: backlight 3s forwards;
animation-delay: 0.3s;
opacity: 0;
}
.character-model canvas {
position: relative;
pointer-events: none;
z-index: 2;
}
.character-hover {
position: absolute;
width: 280px;
height: 280px;
top: 50%;
left: 50%;
z-index: 3;
transform: translate(-50%, -50%);
border-radius: 50%;
}
.landing-intro {
position: absolute;
z-index: 9;
top: 12%;
left: 0;
}
.landing-intro h2 {
margin: 0;
color: var(--accentColor);
font-size: 22px;
font-weight: 300;
letter-spacing: 2px;
}
.landing-intro h1 {
margin: 0;
letter-spacing: 2px;
font-size: 28px;
line-height: 28px;
font-weight: 500;
font-family: "Geist", sans-serif;
}
/* .landing-intro h1 span {
font-weight: 200;
} */
.landing-info {
position: absolute;
right: 50%;
transform: translateX(50%);
bottom: 40px;
top: inherit;
z-index: 9;
}
.landing-info h3 {
font-size: 22px;
letter-spacing: 2px;
font-weight: 300;
color: var(--accentColor);
margin: 0;
}
.landing-info h2 {
margin: 0;
margin-top: -20px;
margin-left: 20px;
font-family: "Geist", sans-serif;
font-weight: 600;
font-size: 32px;
line-height: 40px;
position: relative;
display: flex;
flex-wrap: nowrap;
text-transform: uppercase;
letter-spacing: 2px;
}
.landing-h2-info-1 {
position: absolute;
top: 0;
}
h2.landing-info-h2 {
color: #c481ff;
font-size: 42px;
width: 120%;
margin: 0;
font-family: "Geist", sans-serif;
font-weight: 600;
position: relative;
margin-left: -5px;
}
.landing-h2-2 {
position: absolute;
top: 0;
}
.landing-info-h2::after {
content: "";
position: absolute;
width: 100%;
height: 120%;
z-index: 3;
background-image: linear-gradient(
0deg,
var(--backgroundColor) 40%,
rgba(0, 0, 0, 0) 110%
);
top: 0;
left: 0;
}
@media screen and (min-width: 500px) {
.landing-circle2 {
display: block;
}
.character-model {
z-index: 0;
}
.landing-info h3 {
font-size: 18px;
}
.landing-intro h2 {
font-size: 18px;
}
.landing-intro h1 {
font-size: 30px;
line-height: 30px;
}
.landing-info h2 {
font-size: 35px;
line-height: 40px;
}
h2.landing-info-h2 {
font-size: 38px;
}
}
@media screen and (min-width: 768px) {
.character-model {
height: 80vh;
}
.landing-intro h2 {
font-size: 25px;
}
.landing-intro h1 {
font-size: 40px;
line-height: 35px;
}
.landing-info h3 {
font-size: 25px;
}
.landing-info h2 {
font-size: 45px;
line-height: 42px;
}
h2.landing-info-h2 {
font-size: 55px;
}
}
@media screen and (min-width: 1025px) {
.character-model {
height: 100vh;
bottom: 0;
z-index: 11;
position: fixed;
}
.character-model::after,
.character-model::before {
display: none;
}
.landing-intro {
top: 50%;
left: auto;
right: 66%;
transform: translate(0%, -50%);
}
.landing-info {
bottom: auto;
top: 51%;
z-index: inherit;
text-align: left;
transform: translate(0%, -50%);
right: auto;
left: 66%;
}
}
@media screen and (min-width: 1200px) {
.landing-intro {
top: 50%;
left: auto;
right: 70%;
transform: translate(0%, -50%);
}
.landing-info {
bottom: auto;
top: 51%;
z-index: inherit;
text-align: left;
transform: translate(0%, -50%);
right: auto;
left: 70%;
}
}
@media screen and (min-width: 1600px) {
.landing-intro h2 {
font-size: 35px;
}
.landing-intro h1 {
font-size: 60px;
line-height: 55px;
}
.landing-info h3 {
font-size: 35px;
}
.landing-info h2 {
font-size: 65px;
line-height: 62px;
}
h2.landing-info-h2 {
font-size: 75px;
}
}

View File

@@ -0,0 +1,363 @@
.loading-screen {
position: fixed;
width: 100vw;
height: var(--vh);
/* background-image: linear-gradient(#cbb1ff, #d8c4ff); */
background-color: #eae5ec;
z-index: 999999999;
display: flex;
place-items: center;
justify-content: center;
}
.loading-button {
padding: 20px 50px;
border-radius: 100px;
background-color: #000;
overflow: hidden;
font-size: 18px;
font-weight: 500;
position: relative;
z-index: 9;
}
.loading-button::before {
content: "";
background-color: #ffffff;
top: var(--mouse-y);
left: var(--mouse-x);
border-radius: 50%;
width: 60px;
height: 60px;
opacity: 1;
position: absolute;
z-index: 99;
filter: blur(60px);
opacity: 0;
transform: translate(-50%, -50%);
}
.loading-button:hover::before {
opacity: 1;
}
.loading-clicked .loading-button::before {
opacity: 0;
}
.loading-wrap {
--Lsize: 145px;
padding: 6px;
position: relative;
min-width: 0px;
min-height: 0px;
border-radius: 100px;
background-color: #000;
overflow: hidden;
transition: 0.8s ease-in-out;
transition-delay: 0.2s;
box-shadow: 0px 15px 15px 0px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
}
.loading-clicked {
transition-delay: 0ms;
transition-timing-function: cubic-bezier(0.33, 0.11, 1, 0.72);
transform: scale(1);
min-width: calc(100vw + 5000px);
border-radius: 5000px;
min-height: calc(100vh + 500px);
box-shadow: none;
}
.loading-clicked .loading-button {
overflow: visible;
}
.loading-hover {
background-color: #a87cff;
width: 250px;
height: 120px;
position: absolute;
top: var(--mouse-y);
left: var(--mouse-x);
border-radius: 50%;
transform: translate(-50%, -50%);
filter: blur(30px);
opacity: 1;
transition: opacity 500ms;
}
.loading-wrap:hover .loading-hover {
opacity: 1;
}
.loading-clicked:hover .loading-hover,
.loading-clicked .loading-hover {
opacity: 0;
}
.loading-content {
position: relative;
background-color: #000;
width: 100%;
overflow: hidden;
transition: 0.6s;
text-transform: uppercase;
}
.loading-content-in {
position: relative;
width: var(--Lsize);
overflow: hidden;
}
.loading-content2 {
position: relative;
letter-spacing: 2px;
text-transform: uppercase;
width: var(--Lsize);
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
column-gap: 10px;
text-align: center;
transition: 1s;
max-width: var(--Lsize);
}
.loading-clicked .loading-content2 {
opacity: 0;
transition: 0.5s;
}
.loading-content span {
font-weight: 300;
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
opacity: 0.7;
}
.loading-box {
position: absolute;
right: 0px;
top: 50%;
transform: translate(100%, -50%);
width: 15px;
height: 25px;
background-color: white;
animation: blink 1s linear infinite;
}
.loading-icon {
transform: scale(0);
opacity: 0;
transition: 0.5s;
transition-delay: 0.5s;
}
.loading-complete .loading-icon {
transform: scale(1);
opacity: 1;
}
.loading-clicked .loading-icon {
transition-delay: 0s;
transition: 1s;
transform: translateX(200px);
}
.loading-clicked .loading-content2 {
overflow: visible;
}
.loading-clicked .loading-content2 span {
transition: 1s;
transform: translateY(100px);
opacity: 0;
}
.loading-container {
position: absolute;
width: 100%;
max-width: var(--Lsize);
/* height: 45px; */
top: 50%;
transition: 1s;
left: 50px;
z-index: 9;
transform: translateY(-50%);
}
.loading-complete .loading-container {
max-width: 0px;
}
.loading-header {
width: var(--cWidth);
max-width: var(--cMaxWidth);
position: fixed;
z-index: 9999999999;
display: flex;
justify-content: space-between;
box-sizing: border-box;
padding: 20px 0px;
left: 50%;
transform: translateX(-50%);
top: 0;
color: var(--backgroundColor);
}
.loader-title {
font-weight: 700;
font-size: 14px;
letter-spacing: 0.2px;
}
@keyframes blink {
0% {
opacity: 0;
}
25% {
opacity: 1;
}
75% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.loading-complete .loading-box {
animation: blinkDone 0.3s forwards;
animation-delay: 1s;
opacity: 1;
}
@keyframes blinkDone {
to {
opacity: 0;
}
}
.loaderGame-container {
width: 200px;
transition: 0.3s;
height: 100px;
overflow: hidden;
position: relative;
transform: scale(0.4);
transform-origin: top right;
}
.loader-out .loaderGame-container {
opacity: 0;
}
.loaderGame-in {
width: 1200px;
position: absolute;
overflow: hidden;
left: 0;
animation: loaderGame 7s linear infinite;
}
@keyframes loaderGame {
0% {
transform: translateX(0px);
}
100% {
transform: translateX(-300px);
}
}
.loaderGame-line {
float: left;
margin: 0px 20px;
margin-bottom: 40px;
position: relative;
width: 10px;
height: 60px;
background-color: #000;
display: block;
}
.loaderGame-line:nth-child(2n) {
margin-top: 40px;
margin-bottom: 0px;
}
.loaderGame-ball {
position: absolute;
left: 20%;
top: 0%;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: #a87cff;
animation: ball25 7s infinite;
transform: translateY(10px);
animation-timing-function: cubic-bezier(0.3, 1.18, 0.63, 1.28);
}
.loading-marquee {
position: absolute;
top: 50%;
left: 0;
width: 100%;
transform: translateY(-50%);
color: var(--backgroundColor);
font-size: 60px;
font-weight: 600;
text-transform: uppercase;
}
.loading-marquee span {
padding: 0px 50px;
position: relative;
}
.loading-marquee span::before {
content: "";
width: 20px;
height: 20px;
background-color: var(--backgroundColor);
position: absolute;
top: 50%;
border-radius: 50px;
left: 0px;
transform: translate(-50%, -50%);
}
@keyframes ball25 {
0% {
transform: translateY(70px);
}
15% {
transform: translateY(10px);
}
30% {
transform: translateY(70px);
}
45% {
transform: translateY(10px);
}
67% {
transform: translateY(70px);
}
80% {
transform: translateY(10px);
}
90% {
transform: translateY(70px);
}
100% {
transform: translateY(70px);
}
}
@media only screen and (min-width: 1400px) {
.loading-wrap {
--Lsize: 210px;
}
.loading-button {
padding: 30px 70px;
font-size: 25px;
}
.loading-container {
left: 70px;
}
.loading-marquee {
font-size: 100px;
}
}
@media only screen and (min-width: 500px) {
.loading-header {
padding: 20px 0px;
}
.loader-title {
font-size: 16px;
}
}
@media only screen and (min-width: 1200px) {
.loading-header {
padding: 35px 0px;
}
.loader-title {
font-size: 18px;
}
}

View File

@@ -0,0 +1,87 @@
.header {
display: flex;
max-width: var(--cMaxWidth);
width: var(--cWidth);
justify-content: space-between;
padding: 20px 0px;
margin-bottom: -100px;
box-sizing: border-box;
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 0;
z-index: 9999;
}
.header ul {
font-size: 12px;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
list-style: none;
column-gap: 40px;
row-gap: 8px;
align-items: end;
}
.header ul li {
margin-left: 0px;
letter-spacing: 1px;
color: #ccc;
font-weight: 600;
cursor: pointer;
}
.navbar-connect {
position: absolute;
display: none;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 15px;
letter-spacing: 1px;
font-weight: 500;
}
.navbar-title {
font-family: 'Silkscreen', 'Courier New', monospace;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.2px;
}
@media only screen and (min-width: 500px) {
.header {
padding: 20px 0px;
}
.header ul {
flex-direction: row;
align-items: center;
font-size: 14px;
}
.header ul li {
color: #eae5ec;
}
.navbar-title {
font-family: 'Silkscreen', 'Courier New', monospace;
font-size: 16px;
}
}
@media only screen and (min-width: 900px) {
.navbar-connect {
display: block;
}
}
@media only screen and (min-width: 1200px) {
.header {
padding: 35px 0px;
}
.header ul {
column-gap: 80px;
font-size: 16px;
}
.navbar-connect {
font-size: 16px;
}
.navbar-title {
font-family: 'Silkscreen', 'Courier New', monospace;
font-size: 18px;
}
}

View File

@@ -0,0 +1,103 @@
.icons-section {
position: fixed;
max-width: var(--cMaxWidth);
width: var(--cWidth);
bottom: 0;
z-index: 99;
left: 50%;
transform: translateX(-50%);
}
.social-icons {
position: absolute;
left: -20px;
bottom: 20px;
display: none;
flex-direction: column;
gap: 8px;
z-index: 999;
padding: 10px;
}
.social-icons:hover {
transition: 0.3s;
color: var(--backgroundColor);
}
.social-icons a:hover {
color: var(--backgroundColor);
/* transform: scale(1.2); */
}
.social-icons span {
width: 50px;
height: 50px;
position: relative;
display: flex;
}
.social-icons a {
--siLeft: 50%;
--siTop: 50%;
position: absolute;
left: var(--siLeft, 50%);
top: var(--siTop, 50%);
transform: translate(-50%, -50%);
display: flex;
font-size: 23px;
will-change: left, top;
transition: transform 0.3s ease-out;
}
.resume-button {
position: absolute;
z-index: 99;
display: flex;
gap: 5px;
bottom: 40px;
right: 0;
width: auto;
text-wrap: nowrap;
letter-spacing: 4px;
font-size: 15px;
line-height: 15px;
font-weight: 600;
color: #5e5e5e;
cursor: pointer;
transition: 0.5s;
transform-origin: left bottom;
transform: translateX(100%) rotateZ(-90deg);
}
.resume-button:hover {
color: #fff;
}
div.resume-button span {
color: #fff;
font-size: 17px;
margin-top: -1px;
display: flex;
align-items: center;
}
.check-line {
position: fixed;
top: 655px;
left: 0;
height: 1px;
background-color: #ffffff;
width: 100%;
z-index: 99999;
}
@media only screen and (min-width: 900px) {
.social-icons {
display: flex;
gap: 20px;
}
.social-icons a {
font-size: 28px;
}
}
@media only screen and (min-width: 768px) {
.resume-button {
transform: none;
font-size: 20px;
line-height: 20px;
}
div.resume-button span {
font-size: 23px;
margin-top: -1.5px;
}
}

View File

@@ -0,0 +1,393 @@
.whatIDO {
display: flex;
align-items: center;
justify-content: center;
place-items: center;
position: relative;
opacity: 1;
height: 100vh;
width: var(--cWidth);
max-width: 1920px;
margin: auto;
z-index: 9;
}
.what-box {
width: 50%;
display: flex;
justify-content: center;
position: relative;
z-index: 9;
}
.what-box h2 {
font-size: calc(4vw + 25px);
line-height: calc(4vw + 20px);
font-weight: 600;
margin-right: 10%;
margin-bottom: 100px;
}
.hat-h2 {
font-style: italic;
}
.do-h2 {
color: var(--accentColor);
}
.what-box-in {
flex-direction: column;
height: 500px;
margin-left: 200px;
position: relative;
display: none;
}
.what-content {
width: 450px;
height: 33%;
min-height: 50%;
transition: 0.5s;
/* border: 0.5px dashed rgba(255, 255, 255, 0.3); */
position: relative;
padding: 50px;
box-sizing: border-box;
}
.what-noTouch:hover,
.what-content-active {
min-height: 67%;
padding: 40px 50px;
}
.what-noTouch:hover ~ .what-content,
.what-box-in:hover .what-noTouch:not(:hover),
.what-content.what-sibling {
min-height: 33%;
padding: 10px 50px;
}
.what-content h3 {
font-size: 35px;
letter-spacing: 1px;
margin: 0;
}
.what-content p {
font-size: 14px;
line-height: 18px;
font-weight: 200;
letter-spacing: 0.7px;
}
.what-content h4 {
font-weight: 300;
letter-spacing: 1px;
margin: 0px;
font-size: 14px;
opacity: 0.3;
}
.what-content-in {
opacity: 0;
animation: whatFlicker 0.5s 1 forwards;
animation-delay: 1s;
}
@keyframes whatFlicker {
0%,
25%,
35%,
60% {
opacity: 0;
}
30%,
50%,
40%,
100% {
opacity: 1;
}
}
.what-content::before,
.what-corner::before,
.what-content::after,
.what-corner::after {
content: "";
width: 10px;
height: 10px;
position: absolute;
border: 4px solid #fff;
opacity: 0;
animation: whatCorners 0.2s 1 forwards;
animation-delay: 0.5s;
}
@keyframes whatCorners {
100% {
opacity: 1;
}
}
.what-content::before {
top: -2px;
left: -2px;
border-right: none;
border-bottom: none;
}
.what-corner::before {
top: -2px;
right: -2px;
border-left: none;
border-bottom: none;
}
.what-content::after {
bottom: -2px;
left: -2px;
border-top: none;
border-right: none;
}
.what-corner::after {
bottom: -2px;
right: -2px;
border-left: none;
border-top: none;
}
.what-arrow {
position: absolute;
bottom: 20px;
right: 20px;
width: 25px;
height: 25px;
border: 1px solid #fff;
}
.what-arrow::before {
content: "";
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%) rotate(-45deg);
border-left: 1px solid #fff;
border-bottom: 1px solid #fff;
transition: 0.5s;
width: 10px;
height: 10px;
}
.what-noTouch:hover .what-arrow::before,
.what-content-active .what-arrow::before {
transform: translate(-50%, -20%) rotate(-225deg);
}
.what-border1 {
position: absolute;
top: 0;
width: 100%;
left: 50%;
transform: translateX(-50%);
height: 100%;
transition: 0.5s;
max-width: 0%;
overflow: hidden;
opacity: 0.8;
animation: whatBorders 1.2s 1 forwards;
}
.what-border1 svg {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 450px;
}
.what-border2 {
position: absolute;
top: 50%;
width: 100%;
left: 0;
transform: translateY(-50%);
height: 100%;
max-height: 0%;
overflow: hidden;
transition: 0.5s;
opacity: 0.8;
animation: whatBorders 1.2s 1 forwards;
}
.what-border2 svg {
height: 500px;
top: 50%;
transform: translateY(-50%);
position: absolute;
}
.what-content-in {
height: 100%;
overflow: hidden;
}
.what-content-in h5 {
font-weight: 300;
opacity: 0.5;
font-size: 12px;
letter-spacing: 1px;
font-family: "Geist", sans-serif;
margin-bottom: 5px;
}
@keyframes whatBorders {
80% {
opacity: 0.8;
}
100% {
max-height: 100%;
max-width: 100%;
opacity: 0.2;
}
}
.what-content-flex {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.what-tags {
font-size: 13px;
font-weight: 400;
padding: 2px 7px;
background-color: rgba(255, 255, 255, 0.15);
border: 1px solid #ffffff50;
border-radius: 30px;
}
@media only screen and (max-width: 1600px) {
.what-box h2 {
margin-right: 18%;
}
}
@media only screen and (max-width: 1400px) {
.what-box h2 {
margin-right: 20%;
}
.what-box-in {
height: 400px;
}
.what-content h3 {
font-size: 28px;
}
.what-content {
padding: 30px;
width: 400px;
}
.what-content p {
font-size: 13px;
}
.what-noTouch:hover,
.what-content-active {
padding: 20px 30px;
}
.what-noTouch:hover ~ .what-content,
.what-box-in:hover .what-noTouch:not(:hover),
.what-content.what-sibling {
padding: 10px 30px;
}
.what-tags {
font-size: 12px;
}
}
@media only screen and (max-width: 1400px) {
.what-box-in {
margin-left: 50px;
}
.what-content {
width: 380px;
}
}
@media only screen and (max-width: 1024px) {
.whatIDO {
height: auto;
padding: 50px 0px;
padding-bottom: 50px;
}
.what-box-in {
height: 500px;
margin-left: -50px;
}
.what-content {
padding: 50px;
width: 500px;
}
.what-content p {
font-size: 14px;
}
.what-noTouch:hover,
.what-content-active {
min-height: 67%;
padding: 50px;
}
.what-noTouch:hover ~ .what-content,
.what-box-in:hover .what-noTouch:not(:hover),
.what-content.what-sibling {
min-height: 33%;
padding: 10px 50px;
}
}
@media only screen and (max-width: 900px) {
.whatIDO {
flex-direction: column;
}
.what-box h2 {
margin: 50px 0;
font-size: 55px;
line-height: 53px;
}
.what-box:first-child {
justify-content: left;
}
.what-box:last-child {
height: 500px;
}
.what-box {
width: 500px;
max-width: calc(100% - 50px);
margin: auto;
}
.what-content {
width: 100%;
}
.what-box-in {
margin-left: 0px;
height: 450px;
}
.what-content h5,
.what-content-flex {
opacity: 0;
transition: 0.3s;
}
.what-noTouch:hover h5,
.what-content-active h5,
.what-noTouch:hover .what-content-flex,
.what-content-active .what-content-flex {
opacity: 1;
}
.what-content {
padding: 30px;
}
.what-content p {
font-size: 11px;
}
.what-noTouch:hover,
.what-content-active {
padding: 10px 30px;
}
.what-tags {
font-size: 11px;
}
.what-noTouch:hover ~ .what-content,
.what-box-in:hover .what-noTouch:not(:hover),
.what-content.what-sibling {
padding: 5px 30px;
}
.what-content h3 {
font-size: 25px;
}
}
@media only screen and (max-width: 550px) {
.whatIDO {
place-items: inherit;
align-items: start;
justify-content: left;
}
.what-box {
max-width: calc(100% - 25px);
margin: 0;
}
}
@media only screen and (min-width: 1950px) {
.what-box h2 {
font-size: 7rem;
line-height: 6.8rem;
}
}

View File

@@ -0,0 +1,241 @@
.work-section h2 {
margin-top: 100px;
font-size: 70px;
font-weight: 500;
}
.work-section h2 > span {
color: var(--accentColor);
}
.work-section {
transition: 0s;
height: 100vh;
box-sizing: border-box;
will-change: transform;
}
.work-container {
margin: auto;
display: flex;
flex-direction: column;
height: 100%;
align-content: stretch;
}
.work-flex {
width: 100%;
display: flex;
height: 100%;
margin-left: -80px;
padding-right: 120px;
position: relative;
}
.work-box {
padding: 80px;
display: flex;
flex-direction: column;
width: 600px;
box-sizing: border-box;
border-right: 1px solid #363636;
flex-shrink: 0;
gap: 50px;
justify-content: start;
}
.work-flex .work-box:nth-child(even) {
flex-direction: column-reverse;
}
.work-flex::before,
.work-flex::after {
content: "";
width: calc(50000vw);
height: 1px;
background-color: #363636;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.work-flex::after {
top: 100%;
}
.work-title {
justify-content: space-between;
display: flex;
width: 100%;
margin-bottom: 20px;
}
.work-title > div {
text-align: right;
}
.work-title h3 {
font-size: 50px;
line-height: 50px;
margin: 0;
font-weight: 600;
}
.work-info h4 {
font-size: 18px;
font-weight: 400;
margin: 0;
}
.work-info p {
font-weight: 200;
color: #adacac;
margin: 0;
margin-top: 3px;
}
.work-info > p {
width: 90%;
}
.work-image {
display: flex;
width: 100%;
height: 350px;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.work-image::before {
content: '';
position: absolute;
width: 120%;
height: 120%;
background: radial-gradient(circle at center, rgba(162, 124, 255, 0.6), transparent 60%);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
z-index: 0;
filter: blur(20px);
}
.work-image:hover::before,
.work-image a:hover::before {
opacity: 1;
}
.work-image-in {
position: relative;
width: 100%;
height: 100%;
}
.work-link {
position: absolute;
bottom: 10px;
right: 10px;
background-color: var(--backgroundColor);
width: 50px;
border-radius: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
font-size: 25px;
box-shadow: 0px 0px 10px 0px rgba(255, 255, 255, 0.5),
inset 0px 0px 10px 0px #393939;
transition: 0.3s;
opacity: 0;
}
.work-image a:hover {
color: inherit;
}
.work-image a:hover .work-link {
opacity: 1;
}
.work-image video {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: #000;
object-fit: cover;
}
.work-image img {
width: 100%;
height: 350px;
object-fit: fill;
transition: all 0.3s ease;
position: relative;
z-index: 1;
}
.work-image:hover img,
.work-image a:hover img {
transform: scale(1.05);
filter: brightness(1.1);
}
@media only screen and (max-height: 900px) {
.work-image img {
height: 250px;
}
.work-box {
padding-top: 40px;
padding-bottom: 40px;
}
.work-section h2 {
font-size: 60px;
margin-bottom: 30px;
margin-top: 70px;
}
}
@media only screen and (max-width: 1400px) {
.work-title h3 {
font-size: 35px;
}
.work-info p {
font-size: 13px;
}
.work-info h4 {
font-size: 15px;
}
.work-box {
width: 450px;
padding: 50px;
}
.work-flex {
margin-left: -50px;
padding-right: 75px;
}
.work-section h2 {
font-size: 50px;
}
}
@media only screen and (max-width: 1400px) {
.work-box {
width: 350px;
padding: 30px;
}
.work-flex {
margin-left: -30px;
padding-right: 45px;
}
}
@media only screen and (max-height: 650px) {
.work-image img {
height: 200px;
}
.work-section h2 {
font-size: 40px;
margin-bottom: 20px;
}
.work-box {
gap: 20px;
}
}
/* @media only screen and (max-width: 900px) {
.work-image img {
max-height: 200px;
}
.work-section h2 {
font-size: 40px;
margin-bottom: 20px;
}
.work-box {
gap: 20px;
}
} */
@media only screen and (max-width: 1025px) {
.work-container {
align-content: normal;
}
.work-flex {
height: auto;
}
}

View File

@@ -0,0 +1,20 @@
.hover-link {
position: relative;
display: flex;
text-wrap: nowrap;
overflow: hidden;
}
.hover-in {
position: relative;
transition: 0.3s;
}
.hover-in div {
display: flex;
position: absolute;
top: 100%;
left: 0;
}
.hover-link:hover .hover-in {
transform: translateY(-100%);
color: var(--accentColor);
}

View File

@@ -0,0 +1,191 @@
import * as THREE from "three";
import gsap from "gsap";
export function setCharTimeline(
character: THREE.Object3D<THREE.Object3DEventMap> | null,
camera: THREE.PerspectiveCamera
) {
let intensity: number = 0;
setInterval(() => {
intensity = Math.random();
}, 200);
const tl1 = gsap.timeline({
scrollTrigger: {
trigger: ".landing-section",
start: "top top",
end: "bottom top",
scrub: true,
invalidateOnRefresh: true,
},
});
const tl2 = gsap.timeline({
scrollTrigger: {
trigger: ".about-section",
start: "center 55%",
end: "bottom top",
scrub: true,
invalidateOnRefresh: true,
},
});
const tl3 = gsap.timeline({
scrollTrigger: {
trigger: ".whatIDO",
start: "top top",
end: "bottom top",
scrub: true,
invalidateOnRefresh: true,
},
});
let screenLight: any, monitor: any;
character?.children.forEach((object: any) => {
if (object.name === "Plane004") {
object.children.forEach((child: any) => {
child.material.transparent = true;
child.material.opacity = 0;
if (child.material.name === "Material.027") {
monitor = child;
child.material.color.set("#FFFFFF");
}
});
}
if (object.name === "screenlight") {
object.material.transparent = true;
object.material.opacity = 0;
object.material.emissive.set("#C8BFFF");
gsap.timeline({ repeat: -1, repeatRefresh: true }).to(object.material, {
emissiveIntensity: () => intensity * 8,
duration: () => Math.random() * 0.6,
delay: () => Math.random() * 0.1,
});
screenLight = object;
}
});
let neckBone = character?.getObjectByName("spine005");
if (window.innerWidth > 1024) {
if (character) {
tl1
.fromTo(character.rotation, { y: 0 }, { y: 0.7, duration: 1 }, 0)
.to(camera.position, { z: 22 }, 0)
.fromTo(".character-model", { x: 0 }, { x: "-25%", duration: 1 }, 0)
.to(".landing-container", { opacity: 0, duration: 0.4 }, 0)
.to(".landing-container", { y: "40%", duration: 0.8 }, 0)
.fromTo(".about-me", { y: "-50%" }, { y: "0%" }, 0);
tl2
.to(
camera.position,
{ z: 75, y: 8.4, duration: 6, delay: 2, ease: "power3.inOut" },
0
)
.to(".about-section", { y: "30%", duration: 6 }, 0)
.to(".about-section", { opacity: 0, delay: 3, duration: 2 }, 0)
.fromTo(
".character-model",
{ pointerEvents: "inherit" },
{ pointerEvents: "none", x: "-12%", delay: 2, duration: 5 },
0
)
.to(character.rotation, { y: 0.92, x: 0.12, delay: 3, duration: 3 }, 0)
.to(neckBone!.rotation, { x: 0.6, delay: 2, duration: 3 }, 0)
.to(monitor.material, { opacity: 1, duration: 0.8, delay: 3.2 }, 0)
.to(screenLight.material, { opacity: 1, duration: 0.8, delay: 4.5 }, 0)
.fromTo(
".what-box-in",
{ display: "none" },
{ display: "flex", duration: 0.1, delay: 6 },
0
)
.fromTo(
monitor.position,
{ y: -10, z: 2 },
{ y: 0, z: 0, delay: 1.5, duration: 3 },
0
)
.fromTo(
".character-rim",
{ opacity: 1, scaleX: 1.4 },
{ opacity: 0, scale: 0, y: "-70%", duration: 5, delay: 2 },
0.3
);
tl3
.fromTo(
".character-model",
{ y: "0%" },
{ y: "-100%", duration: 4, ease: "none", delay: 1 },
0
)
.fromTo(".whatIDO", { y: 0 }, { y: "15%", duration: 2 }, 0)
.to(character.rotation, { x: -0.04, duration: 2, delay: 1 }, 0);
}
} else {
if (character) {
const tM2 = gsap.timeline({
scrollTrigger: {
trigger: ".what-box-in",
start: "top 70%",
end: "bottom top",
},
});
tM2.to(".what-box-in", { display: "flex", duration: 0.1, delay: 0 }, 0);
}
}
}
export function setAllTimeline() {
const careerTimeline = gsap.timeline({
scrollTrigger: {
trigger: ".career-section",
start: "top 30%",
end: "100% center",
scrub: true,
invalidateOnRefresh: true,
},
});
careerTimeline
.fromTo(
".career-timeline",
{ maxHeight: "10%" },
{ maxHeight: "100%", duration: 0.5 },
0
)
.fromTo(
".career-timeline",
{ opacity: 0 },
{ opacity: 1, duration: 0.1 },
0
)
.fromTo(
".career-info-box",
{ opacity: 0 },
{ opacity: 1, stagger: 0.1, duration: 0.5 },
0
)
.fromTo(
".career-dot",
{ animationIterationCount: "infinite" },
{
animationIterationCount: "1",
delay: 0.3,
duration: 0.1,
},
0
);
if (window.innerWidth > 1024) {
careerTimeline.fromTo(
".career-section",
{ y: 0 },
{ y: "20%", duration: 0.5, delay: 0.2 },
0
);
} else {
careerTimeline.fromTo(
".career-section",
{ y: 0 },
{ y: 0, duration: 0.5, delay: 0.2 },
0
);
}
}

View File

@@ -0,0 +1,136 @@
import { SplitText } from "gsap/dist/SplitText";
import gsap from "gsap";
import { smoother } from "../Navbar";
export function initialFX() {
document.body.style.overflowY = "auto";
smoother.paused(false);
document.getElementsByTagName("main")[0].classList.add("main-active");
gsap.to("body", {
backgroundColor: "#0b080c",
duration: 0.5,
delay: 1,
});
var landingText = new SplitText(
[".landing-info h3", ".landing-intro h2", ".landing-intro h1"],
{
type: "chars,lines",
linesClass: "split-line",
}
);
gsap.fromTo(
landingText.chars,
{ opacity: 0, y: 80, filter: "blur(5px)" },
{
opacity: 1,
duration: 1.2,
filter: "blur(0px)",
ease: "power3.inOut",
y: 0,
stagger: 0.025,
delay: 0.3,
}
);
let TextProps = { type: "chars,lines", linesClass: "split-h2" };
var landingText2 = new SplitText(".landing-h2-info", TextProps);
gsap.fromTo(
landingText2.chars,
{ opacity: 0, y: 80, filter: "blur(5px)" },
{
opacity: 1,
duration: 1.2,
filter: "blur(0px)",
ease: "power3.inOut",
y: 0,
stagger: 0.025,
delay: 0.3,
}
);
gsap.fromTo(
".landing-info-h2",
{ opacity: 0, y: 30 },
{
opacity: 1,
duration: 1.2,
ease: "power1.inOut",
y: 0,
delay: 0.8,
}
);
gsap.fromTo(
[".header", ".icons-section", ".nav-fade"],
{ opacity: 0 },
{
opacity: 1,
duration: 1.2,
ease: "power1.inOut",
delay: 0.1,
}
);
var landingText3 = new SplitText(".landing-h2-info-1", TextProps);
var landingText4 = new SplitText(".landing-h2-1", TextProps);
var landingText5 = new SplitText(".landing-h2-2", TextProps);
LoopText(landingText2, landingText3);
LoopText(landingText4, landingText5);
}
function LoopText(Text1: SplitText, Text2: SplitText) {
var tl = gsap.timeline({ repeat: -1, repeatDelay: 1 });
const delay = 4;
const delay2 = delay * 2 + 1;
tl.fromTo(
Text2.chars,
{ opacity: 0, y: 80 },
{
opacity: 1,
duration: 1.2,
ease: "power3.inOut",
y: 0,
stagger: 0.1,
delay: delay,
},
0
)
.fromTo(
Text1.chars,
{ y: 80 },
{
duration: 1.2,
ease: "power3.inOut",
y: 0,
stagger: 0.1,
delay: delay2,
},
1
)
.fromTo(
Text1.chars,
{ y: 0 },
{
y: -80,
duration: 1.2,
ease: "power3.inOut",
stagger: 0.1,
delay: delay,
},
0
)
.to(
Text2.chars,
{
y: -80,
duration: 1.2,
ease: "power3.inOut",
stagger: 0.1,
delay: delay2,
},
1
);
}

View File

@@ -0,0 +1,80 @@
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { ScrollSmoother } from "gsap/dist/ScrollSmoother";
import { SplitText } from "gsap/dist/SplitText";
interface ParaElement extends HTMLElement {
anim?: gsap.core.Animation;
split?: SplitText;
}
gsap.registerPlugin(ScrollTrigger, ScrollSmoother, SplitText);
export default function setSplitText() {
ScrollTrigger.config({ ignoreMobileResize: true });
if (window.innerWidth < 900) return;
const paras: NodeListOf<ParaElement> = document.querySelectorAll(".para");
const titles: NodeListOf<ParaElement> = document.querySelectorAll(".title");
const TriggerStart = window.innerWidth <= 1024 ? "top 60%" : "20% 60%";
const ToggleAction = "play pause resume reverse";
paras.forEach((para: ParaElement) => {
para.classList.add("visible");
if (para.anim) {
para.anim.progress(1).kill();
para.split?.revert();
}
para.split = new SplitText(para, {
type: "lines,words",
linesClass: "split-line",
});
para.anim = gsap.fromTo(
para.split.words,
{ autoAlpha: 0, y: 80 },
{
autoAlpha: 1,
scrollTrigger: {
trigger: para.parentElement?.parentElement,
toggleActions: ToggleAction,
start: TriggerStart,
},
duration: 1,
ease: "power3.out",
y: 0,
stagger: 0.02,
}
);
});
titles.forEach((title: ParaElement) => {
if (title.anim) {
title.anim.progress(1).kill();
title.split?.revert();
}
title.split = new SplitText(title, {
type: "chars,lines",
linesClass: "split-line",
});
title.anim = gsap.fromTo(
title.split.chars,
{ autoAlpha: 0, y: 80, rotate: 10 },
{
autoAlpha: 1,
scrollTrigger: {
trigger: title.parentElement?.parentElement,
toggleActions: ToggleAction,
start: TriggerStart,
},
duration: 0.8,
ease: "power2.inOut",
y: 0,
rotate: 0,
stagger: 0.03,
}
);
});
ScrollTrigger.addEventListener("refresh", () => setSplitText());
}