From aa0464e12bfde9d333ea41c65dac5e5150736d3c Mon Sep 17 00:00:00 2001 From: Kevin Riehl Date: Sun, 11 Jan 2026 23:39:53 -0800 Subject: [PATCH] Added welcome animation --- src/lib/components/SkillTree.svelte | 313 +++++++++++++++++++++++----- 1 file changed, 262 insertions(+), 51 deletions(-) diff --git a/src/lib/components/SkillTree.svelte b/src/lib/components/SkillTree.svelte index 940a6d5..622571a 100644 --- a/src/lib/components/SkillTree.svelte +++ b/src/lib/components/SkillTree.svelte @@ -7,6 +7,7 @@ let container: HTMLDivElement; let selectedNode: TreeNode | null = null; let modalVisible = false; + let showWelcomeTitle = true; // Tooltip state let hoveredNode: TreeNode | null = null; @@ -365,9 +366,17 @@ const treeContainer = new Container(); app.stage.addChild(treeContainer); + // Initial position (centered) treeContainer.x = app.screen.width / 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 ========== let isDragging = false; let hasMoved = false; @@ -743,59 +752,161 @@ } } - // ========== ENTRANCE ANIMATIONS ========== - lineObjects - .sort((a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node)) - .forEach((line, index) => { - gsap.to(line.graphics, { - alpha: 1, - duration: 0.4, - delay: index * 0.06, - ease: 'power2.out', - }); - gsap.to(line.glowGraphics, { - alpha: 1, - duration: 0.6, - delay: index * 0.06 + 0.1, - ease: 'power2.out', - }); + // ========== ENTRANCE ANIMATIONS (Sequenced) ========== + + // Timeline: + // 0.0s - 2.5s: Welcome title displays (handled by CSS) + // 2.5s: Title starts fading, origin node appears + // 3.0s: Zoom out begins, tree starts populating + // 6.0s: Animation complete + + // Responsive animation timing + const titleDuration = isMobile ? 1.5 : 2.5; + const zoomDuration = isMobile ? 2.5 : 4.5; + const nodeStagger = isMobile ? 0.05 : 0.08; + + // Hide all lines initially + 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', }); - - nodeObjects - .sort((a, b) => getDistanceFromOrigin(a.node) - getDistanceFromOrigin(b.node)) - .forEach((obj, index) => { - const delay = index * 0.06; - - 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.graphics.scale, { + x: 1, + y: 1, + duration: originDuration, + delay: originDelay, + ease: 'back.out(1.7)', }); + 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!'); @@ -828,6 +939,14 @@
+ +{#if showWelcomeTitle} +
+

Kevin Riehl

+

The Atlas of Skills

+
+{/if} + {#if hoveredNode && !modalVisible && !isMobile}
\ No newline at end of file