Added welcome animation
This commit is contained in:
1 parent
fb2df94db1
commit
aa0464e12b
1 file changed
+262
-51
@@ -7,6 +7,7 @@
|
|||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let selectedNode: TreeNode | null = null;
|
let selectedNode: TreeNode | null = null;
|
||||||
let modalVisible = false;
|
let modalVisible = false;
|
||||||
|
let showWelcomeTitle = true;
|
||||||
|
|
||||||
// Tooltip state
|
// Tooltip state
|
||||||
let hoveredNode: TreeNode | null = null;
|
let hoveredNode: TreeNode | null = null;
|
||||||
@@ -365,9 +366,17 @@
|
|||||||
const treeContainer = new Container();
|
const treeContainer = new Container();
|
||||||
app.stage.addChild(treeContainer);
|
app.stage.addChild(treeContainer);
|
||||||
|
|
||||||
|
// Initial position (centered)
|
||||||
treeContainer.x = app.screen.width / 2;
|
treeContainer.x = app.screen.width / 2;
|
||||||
treeContainer.y = app.screen.height / 2;
|
treeContainer.y = app.screen.height / 2;
|
||||||
|
|
||||||
|
// Start zoomed in - will zoom out after welcome title
|
||||||
|
// Mobile needs to zoom out further to show more of the tree
|
||||||
|
const startScale = isMobile ? 3 : 4;
|
||||||
|
const endScale = isMobile ? 0.6 : 1;
|
||||||
|
treeContainer.scale.set(startScale);
|
||||||
|
treeContainer.alpha = 1; // Tree is visible but zoomed in
|
||||||
|
|
||||||
// ========== PAN & ZOOM STATE ==========
|
// ========== PAN & ZOOM STATE ==========
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let hasMoved = false;
|
let hasMoved = false;
|
||||||
@@ -743,59 +752,161 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ENTRANCE ANIMATIONS ==========
|
// ========== ENTRANCE ANIMATIONS (Sequenced) ==========
|
||||||
lineObjects
|
|
||||||
.sort((a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node))
|
// Timeline:
|
||||||
.forEach((line, index) => {
|
// 0.0s - 2.5s: Welcome title displays (handled by CSS)
|
||||||
gsap.to(line.graphics, {
|
// 2.5s: Title starts fading, origin node appears
|
||||||
alpha: 1,
|
// 3.0s: Zoom out begins, tree starts populating
|
||||||
duration: 0.4,
|
// 6.0s: Animation complete
|
||||||
delay: index * 0.06,
|
|
||||||
ease: 'power2.out',
|
// Responsive animation timing
|
||||||
});
|
const titleDuration = isMobile ? 1.5 : 2.5;
|
||||||
gsap.to(line.glowGraphics, {
|
const zoomDuration = isMobile ? 2.5 : 4.5;
|
||||||
alpha: 1,
|
const nodeStagger = isMobile ? 0.05 : 0.08;
|
||||||
duration: 0.6,
|
|
||||||
delay: index * 0.06 + 0.1,
|
// Hide all lines initially
|
||||||
ease: 'power2.out',
|
lineObjects.forEach((line) => {
|
||||||
});
|
line.graphics.alpha = 0;
|
||||||
|
line.glowGraphics.alpha = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide all nodes initially
|
||||||
|
nodeObjects.forEach((obj) => {
|
||||||
|
obj.graphics.alpha = 0;
|
||||||
|
obj.graphics.scale.set(0.5);
|
||||||
|
obj.glowGraphics.alpha = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort nodes by distance from origin
|
||||||
|
const sortedNodes = [...nodeObjects].sort(
|
||||||
|
(a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node)
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedLines = [...lineObjects].sort(
|
||||||
|
(a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node)
|
||||||
|
);
|
||||||
|
|
||||||
|
// STEP 1: After title fades, show origin node first
|
||||||
|
// On mobile, start origin animation BEFORE title fully fades (overlap them)
|
||||||
|
const originNode = sortedNodes.find(obj => obj.node.id === 'origin');
|
||||||
|
const originDelay = isMobile ? titleDuration - 0.5 : titleDuration;
|
||||||
|
const originDuration = isMobile ? 0.3 : 0.6;
|
||||||
|
|
||||||
|
// Pre-set origin node to be ready (reduces perceived delay on mobile)
|
||||||
|
if (originNode && isMobile) {
|
||||||
|
originNode.graphics.scale.set(0.8); // Start slightly larger
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originNode) {
|
||||||
|
gsap.to(originNode.graphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: originDuration,
|
||||||
|
delay: originDelay,
|
||||||
|
ease: 'power2.out',
|
||||||
});
|
});
|
||||||
|
gsap.to(originNode.graphics.scale, {
|
||||||
nodeObjects
|
x: 1,
|
||||||
.sort((a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node))
|
y: 1,
|
||||||
.forEach((obj, index) => {
|
duration: originDuration,
|
||||||
const delay = index * 0.06;
|
delay: originDelay,
|
||||||
|
ease: 'back.out(1.7)',
|
||||||
gsap.to(obj.graphics, {
|
|
||||||
alpha: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
delay: delay,
|
|
||||||
ease: 'back.out(1.7)',
|
|
||||||
});
|
|
||||||
gsap.to(obj.graphics.scale, {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
delay: delay,
|
|
||||||
ease: 'back.out(1.7)',
|
|
||||||
});
|
|
||||||
gsap.to(obj.glowGraphics, {
|
|
||||||
alpha: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
delay: delay,
|
|
||||||
ease: 'power2.out',
|
|
||||||
});
|
|
||||||
|
|
||||||
gsap.to(obj.glowGraphics.scale, {
|
|
||||||
x: 1.4,
|
|
||||||
y: 1.4,
|
|
||||||
duration: 2 + Math.random() * 1,
|
|
||||||
delay: delay + 0.8,
|
|
||||||
ease: 'sine.inOut',
|
|
||||||
yoyo: true,
|
|
||||||
repeat: -1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
gsap.to(originNode.glowGraphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: originDuration,
|
||||||
|
delay: originDelay,
|
||||||
|
ease: 'power2.out',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: Start zoom out after origin appears
|
||||||
|
const zoomDelay = isMobile ? titleDuration - 0.3 : titleDuration + 0.3;
|
||||||
|
|
||||||
|
gsap.to(treeContainer.scale, {
|
||||||
|
x: endScale,
|
||||||
|
y: endScale,
|
||||||
|
duration: zoomDuration,
|
||||||
|
delay: zoomDelay,
|
||||||
|
ease: 'power1.out',
|
||||||
|
});
|
||||||
|
|
||||||
|
// STEP 3: Animate in the rest of the nodes (excluding origin)
|
||||||
|
const otherNodes = sortedNodes.filter(obj => obj.node.id !== 'origin');
|
||||||
|
const nodeStartDelay = isMobile ? titleDuration - 0.2 : titleDuration + 0.5;
|
||||||
|
|
||||||
|
otherNodes.forEach((obj, index) => {
|
||||||
|
const delay = nodeStartDelay + (index * nodeStagger);
|
||||||
|
|
||||||
|
gsap.to(obj.graphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
delay: delay,
|
||||||
|
ease: 'power2.out',
|
||||||
|
});
|
||||||
|
gsap.to(obj.graphics.scale, {
|
||||||
|
x: 1,
|
||||||
|
y: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
delay: delay,
|
||||||
|
ease: 'back.out(1.7)',
|
||||||
|
});
|
||||||
|
gsap.to(obj.glowGraphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: 0.5,
|
||||||
|
delay: delay,
|
||||||
|
ease: 'power2.out',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start ambient pulse after node appears
|
||||||
|
gsap.to(obj.glowGraphics.scale, {
|
||||||
|
x: 1.4,
|
||||||
|
y: 1.4,
|
||||||
|
duration: 2 + Math.random() * 1,
|
||||||
|
delay: delay + 0.5,
|
||||||
|
ease: 'sine.inOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start origin pulse too
|
||||||
|
if (originNode) {
|
||||||
|
gsap.to(originNode.glowGraphics.scale, {
|
||||||
|
x: 1.4,
|
||||||
|
y: 1.4,
|
||||||
|
duration: 2 + Math.random() * 1,
|
||||||
|
delay: titleDuration + 1,
|
||||||
|
ease: 'sine.inOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 4: Animate lines as nodes appear
|
||||||
|
const lineStartDelay = isMobile ? titleDuration - 0.3 : titleDuration + 0.4;
|
||||||
|
|
||||||
|
sortedLines.forEach((line, index) => {
|
||||||
|
const delay = lineStartDelay + (index * nodeStagger);
|
||||||
|
|
||||||
|
gsap.to(line.graphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
delay: delay,
|
||||||
|
ease: 'power2.out',
|
||||||
|
});
|
||||||
|
gsap.to(line.glowGraphics, {
|
||||||
|
alpha: 1,
|
||||||
|
duration: 0.6,
|
||||||
|
delay: delay + 0.1,
|
||||||
|
ease: 'power2.out',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up title card after animation (matches titleDuration)
|
||||||
|
setTimeout(() => {
|
||||||
|
showWelcomeTitle = false;
|
||||||
|
}, isMobile ? 1500 : 2500);
|
||||||
|
|
||||||
console.log('✨ Responsive skill tree loaded!');
|
console.log('✨ Responsive skill tree loaded!');
|
||||||
|
|
||||||
@@ -828,6 +939,14 @@
|
|||||||
|
|
||||||
<div bind:this={container} class="canvas-container"></div>
|
<div bind:this={container} class="canvas-container"></div>
|
||||||
|
|
||||||
|
<!-- Welcome Title Card -->
|
||||||
|
{#if showWelcomeTitle}
|
||||||
|
<div class="welcome-title" class:fade-out={!showWelcomeTitle}>
|
||||||
|
<h1>Kevin Riehl</h1>
|
||||||
|
<p>The Atlas of Skills</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Hover Tooltip (hidden on mobile) -->
|
<!-- Hover Tooltip (hidden on mobile) -->
|
||||||
{#if hoveredNode && !modalVisible && !isMobile}
|
{#if hoveredNode && !modalVisible && !isMobile}
|
||||||
<div
|
<div
|
||||||
@@ -1396,4 +1515,96 @@
|
|||||||
background: #4a5568;
|
background: #4a5568;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: titleFadeInOut 2.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.welcome-title {
|
||||||
|
animation: titleFadeInOutMobile 1.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes titleFadeInOutMobile {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.9);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #ffd700;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow:
|
||||||
|
0 0 20px rgba(255, 215, 0, 0.8),
|
||||||
|
0 0 40px rgba(255, 215, 0, 0.5),
|
||||||
|
0 0 60px rgba(255, 215, 0, 0.3);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #a8a8b8;
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes titleFadeInOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.9);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
85% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.welcome-title h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title p {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user