diff --git a/packages/compass-welcome/src/components/connection-plug.tsx b/packages/compass-welcome/src/components/connection-plug.tsx new file mode 100644 index 00000000000..0c317781091 --- /dev/null +++ b/packages/compass-welcome/src/components/connection-plug.tsx @@ -0,0 +1,446 @@ +import React, { useMemo, useState, useLayoutEffect, useRef } from 'react'; +import { useConnectionIds } from '@mongodb-js/compass-connections/provider'; + +const CONNECT_ANIMATION_DURATION_MS = 600; +const DISCONNECT_ANIMATION_DURATION_MS = 200; + +// Only animate the sparks over last % of connection animation. +const SPARK_DURATION_PERCENTAGE_OF_TOTAL = 0.35; +const SPARK_TRAVEL_DISTANCE = 44; // pixels + +const pluginDirection = Math.PI * (14.63 / 8); +const inversePluginDirection = pluginDirection + Math.PI; + +const DISTANCE_TO_MOVE = 29.7; + +function useIsAConnectionConnected() { + const connectedConnectionIds = useConnectionIds( + (connection) => connection.status === 'connected' + ); + const isConnected = useMemo( + () => connectedConnectionIds.length > 0, + [connectedConnectionIds] + ); + return isConnected; +} + +// Easing function for smoother animation, the slight pull back and accelerate. +function easeWithSpring(t: number): number { + return t * t * (3.7 * t - 2.7); +} + +const LIGHTNING_COLOR = '#FFE212'; +const LIGHTNING_STROKE_COLOR = '#8a7b0bff'; + +function LightningSparks({ + offsetX, + offsetY, + rotation, + sparkOffsetDirection, + sparkOffsetAmount, + scale, +}: { + offsetX: number; + offsetY: number; + rotation: number; + sparkOffsetDirection: number; + sparkOffsetAmount: number; + scale: number; +}) { + const sparkOffsetX = + offsetX + Math.cos(sparkOffsetDirection) * sparkOffsetAmount; + const sparkOffsetY = + offsetY + Math.sin(sparkOffsetDirection) * sparkOffsetAmount; + + return ( + + + {/* Main lightning bolt - centered around 0,0 */} + + + {/* Secondary smaller lightning bolts */} + + + + + ); +} + +// Shows a plug that animates through the connection process. +export function ConnectionPlug() { + const isConnected = useIsAConnectionConnected(); + const animationStartTime = useRef(Date.now()); + const animationFrameRef = useRef(null); + + // 0 = disconnected/animation start, 1 = connected/animation complete. + const [animationProgress, setAnimationProgress] = useState( + isConnected ? 1 : 0 + ); + + useLayoutEffect(() => { + const cancelOngoingAnimation = () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + }; + + if (isConnected) { + cancelOngoingAnimation(); + animationStartTime.current = Date.now(); + + const animate = () => { + const elapsed = Date.now() - animationStartTime.current; + const rawProgress = Math.min( + elapsed / CONNECT_ANIMATION_DURATION_MS, + 1 + ); + const easedProgress = easeWithSpring(rawProgress); + + setAnimationProgress((currentProgress) => + currentProgress >= 0.99999 ? currentProgress : easedProgress + ); + + if (rawProgress < 0.99999) { + animationFrameRef.current = requestAnimationFrame(animate); + } + }; + + animationFrameRef.current = requestAnimationFrame(animate); + } else if (!isConnected) { + cancelOngoingAnimation(); + // Quick disconnect animation. + animationStartTime.current = Date.now(); + + const animate = () => { + const elapsed = Date.now() - animationStartTime.current; + const rawProgress = Math.min( + elapsed / DISCONNECT_ANIMATION_DURATION_MS, + 1 + ); + const newProgress = 1 - rawProgress; + + if (rawProgress > 0.000001) { + animationFrameRef.current = requestAnimationFrame(animate); + } + + setAnimationProgress((currentProgress) => + currentProgress <= 0.000001 ? currentProgress : newProgress + ); + }; + + animationFrameRef.current = requestAnimationFrame(animate); + } + + return cancelOngoingAnimation; + }, [isConnected]); + + const leftPlugOffsetX = + animationProgress * DISTANCE_TO_MOVE * Math.cos(pluginDirection); + const leftPlugOffsetY = + animationProgress * DISTANCE_TO_MOVE * Math.sin(pluginDirection); + + const rightPlugOffsetX = + animationProgress * DISTANCE_TO_MOVE * Math.cos(inversePluginDirection); + const rightPlugOffsetY = + animationProgress * DISTANCE_TO_MOVE * Math.sin(inversePluginDirection); + + const sparkAnimationProgress = Math.min( + Math.max( + (animationProgress - (1 - SPARK_DURATION_PERCENTAGE_OF_TOTAL)) / + SPARK_DURATION_PERCENTAGE_OF_TOTAL, + 0 + ), + 1 + ); + + // Spark appears when animation is nearly complete and fades out. + const sparkOpacity = + isConnected && animationProgress > 1 - SPARK_DURATION_PERCENTAGE_OF_TOTAL + ? (() => { + const fadeInProgress = sparkAnimationProgress * 5; + const fadeOutProgress = Math.pow(1 - sparkAnimationProgress, 4); + + return fadeInProgress * fadeOutProgress * 5; + })() + : 0; + + const sparkScale = sparkAnimationProgress * 1.5; + const sparkOffsetAmount = + sparkAnimationProgress * sparkAnimationProgress * SPARK_TRAVEL_DISTANCE; + + return ( + + {/* Background. */} + + + {/* Wire bases */} + + + + + + + + {/* Right plug wire */} + + + {/* Left plug sockets, we want these to hide behind the right plug */} + + + + + + {/* Right plug - the parts we to cover the left sockets */} + + + + + + + + + + + + + + {/* Left plug wire */} + + + {/* Left plug - animate rightward as it connects */} + + + + + + + + + + + + {/* Spark animation - appears when plugs connect */} + + + + + + ); +} diff --git a/packages/compass-welcome/src/components/desktop-welcome-tab.tsx b/packages/compass-welcome/src/components/desktop-welcome-tab.tsx index 5c320d67f76..454e0ffcf62 100644 --- a/packages/compass-welcome/src/components/desktop-welcome-tab.tsx +++ b/packages/compass-welcome/src/components/desktop-welcome-tab.tsx @@ -18,7 +18,8 @@ import { import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; import { useConnectionActions } from '@mongodb-js/compass-connections/provider'; import { usePreference } from 'compass-preferences-model/provider'; -import { WelcomeTabImage, WelcomePlugImage } from './welcome-image'; +import { WelcomeTabImage } from './welcome-image'; +import { ConnectionPlug } from './connection-plug'; import ConnectionList, { useActiveConnectionIds } from './connection-list'; const sectionContainerStyles = css({ @@ -131,7 +132,7 @@ export default function DesktopWelcomeTab() { return (
- {activeConnectionIds.length ? : } + {activeConnectionIds.length ? : }

Welcome to MongoDB Compass

{!activeConnectionIds.length && enableCreatingNewConnections ? ( diff --git a/packages/compass-welcome/src/components/web-welcome-tab.spec.tsx b/packages/compass-welcome/src/components/web-welcome-tab.spec.tsx index 66b013b217e..ca5b6847fc2 100644 --- a/packages/compass-welcome/src/components/web-welcome-tab.spec.tsx +++ b/packages/compass-welcome/src/components/web-welcome-tab.spec.tsx @@ -13,7 +13,7 @@ const CONNECTION_ITEM = { }; const renderWebWelcomeTab = (connections: ConnectionInfo[] = []) => { - renderWithConnections(, { + return renderWithConnections(, { connections, }); }; @@ -58,5 +58,17 @@ describe('WebWelcomeTab', function () { // noop } }); + it('does not render the connection plug SVG', function () { + renderWebWelcomeTab([CONNECTION_ITEM]); + expect(screen.queryByTestId('connection-plug-svg')).to.not.exist; + }); + }); + + context('with at least one active connection', function () { + it('renders the connection plug SVG', async function () { + const renderResult = renderWebWelcomeTab([CONNECTION_ITEM]); + await renderResult.connectionsStore.actions.connect(CONNECTION_ITEM); + expect(screen.getByTestId('connection-plug-svg')).to.be.visible; + }); }); }); diff --git a/packages/compass-welcome/src/components/web-welcome-tab.tsx b/packages/compass-welcome/src/components/web-welcome-tab.tsx index da7b3213dea..364225c7dfa 100644 --- a/packages/compass-welcome/src/components/web-welcome-tab.tsx +++ b/packages/compass-welcome/src/components/web-welcome-tab.tsx @@ -9,8 +9,9 @@ import { Link, } from '@mongodb-js/compass-components'; import { useConnectionIds } from '@mongodb-js/compass-connections/provider'; -import { WelcomePlugImage, WelcomeTabImage } from './welcome-image'; +import { WelcomeTabImage } from './welcome-image'; import ConnectionList, { useActiveConnectionIds } from './connection-list'; +import { ConnectionPlug } from './connection-plug'; const welcomeTabStyles = css({ display: 'flex', @@ -33,7 +34,7 @@ export default function WebWelcomeTab() { return (
- {activeConnectionIds.length ? : } + {activeConnectionIds.length ? : }

Welcome! Explore your data

{!activeConnectionIds.length && ( diff --git a/packages/compass-welcome/src/components/welcome-image.tsx b/packages/compass-welcome/src/components/welcome-image.tsx index 6ad6aae9b84..4fae117cec9 100644 --- a/packages/compass-welcome/src/components/welcome-image.tsx +++ b/packages/compass-welcome/src/components/welcome-image.tsx @@ -427,214 +427,3 @@ export function WelcomeTabImage(props: SVGProps) { ); } - -export function WelcomePlugImage(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}