diff --git a/App.tsx b/App.tsx index 8a2968c..8e9d58f 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { createContext, useCallback, useRef, useState } from "react"; import { StyleSheet, View, useColorScheme } from "react-native"; import Home from "./app/Home"; import { ThemeProvider } from "styled-components/native"; @@ -9,6 +9,10 @@ import { SafeAreaProvider } from "react-native-safe-area-context"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import lightTheme from "./styles/themes/light"; import darkTheme from "./styles/themes/dark"; +import ToastHost from "./components/ToastHost"; +import AnimatedSplashScreen from "./components/SplashScreen"; +import { FadeOutDown, SlideOutDown } from "react-native-reanimated"; +import LottieView from "lottie-react-native"; SplashScreen.preventAutoHideAsync(); @@ -18,11 +22,24 @@ SplashScreen.preventAutoHideAsync(); // fade: true, // }); +type ContextType = { setup: boolean; hideSplash: () => void }; +export const Context = createContext({ + setup: false, + hideSplash: () => {}, +}); + export default function App() { + const lottieViewRef = useRef(null); + const [setup, setSetup] = useState(false); const [fontsLoaded, fontError] = useFonts(fontObject); const colorScheme = useColorScheme(); const theme = colorScheme === "dark" ? darkTheme : lightTheme; + const hideSplash: ContextType["hideSplash"] = () => { + lottieViewRef.current?.play(); + setSetup(true); + }; + const onLayoutRootView = useCallback(async () => { if (fontsLoaded || fontError) { await SplashScreen.hideAsync(); @@ -35,19 +52,30 @@ export default function App() { return ( - - - - - - - + + + + + {!setup && ( + + )} + + + + + + ); } diff --git a/app.config.js b/app.config.js index a585a12..662a98e 100644 --- a/app.config.js +++ b/app.config.js @@ -7,21 +7,21 @@ export default { platforms: ["ios", "android"], version: "1.0.3", orientation: "portrait", - icon: "./assets/icon.png", + icon: "./assets/images/icon.png", userInterfaceStyle: "automatic", splash: { - image: "./assets/splashLight.png", + image: "./assets/images/splashLight.png", resizeMode: "cover", backgroundColor: "#812228", dark: { - image: "./assets/splashDark.png", + image: "./assets/images/splashDark.png", resizeMode: "cover", backgroundColor: "#37000B", }, }, assetBundlePatterns: ["**/*"], ios: { - icon: "./assets/GopherTunnels.icon", + icon: "./assets/images/GopherTunnels.icon", supportsTablet: false, bundleIdentifier: "com.adcumn.gophertunnels", infoPlist: { @@ -31,14 +31,14 @@ export default { }, android: { adaptiveIcon: { - foregroundImage: "./assets/adaptive-icon.png", + foregroundImage: "./assets/images/adaptive-icon.png", backgroundColor: "#ffffff", }, package: "com.adcumn.gophertunnels", versionCode: 103, }, web: { - favicon: "./assets/adaptive-icon.png", + favicon: "./assets/images/adaptive-icon.png", }, plugins: [ "expo-router", diff --git a/app/Home/index.tsx b/app/Home/index.tsx index 6f386ab..bc47199 100644 --- a/app/Home/index.tsx +++ b/app/Home/index.tsx @@ -4,14 +4,20 @@ import React, { useRef, useCallback, useLayoutEffect, + useContext, } from "react"; import { StyleSheet } from "react-native"; import * as Location from "expo-location"; import CustomMarker from "../../components/CustomMarker"; import * as SplashScreen from "expo-splash-screen"; -import Mapbox, { UserTrackingMode, type Location as MapboxLocation } from "@rnmapbox/maps"; +import Mapbox, { + UserTrackingMode, + type Location as MapboxLocation, +} from "@rnmapbox/maps"; import { MAPBOX_ACCESS_TOKEN } from "../../mapboxConfig"; +import { toast } from "sonner-native"; +import NetInfo, { type NetInfoState } from "@react-native-community/netinfo"; import { Container, Content } from "./styles"; import SearchBar from "../../components/Searchbar"; @@ -19,15 +25,22 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import DirectionsHeader from "../../components/DirectionsHeader"; import BottomControls from "../../components/BottomControls"; import { devLog, getBoundingBox, metersToMiles } from "../../utils/functions"; -import { getBuildings, getRoute } from "../../services/api"; +import { getBuildings, getPopular, getRoute } from "../../services/api"; import PathComponent from "../../components/PathComponent"; -import { GetBuildingsResponse, GetRouteResponse } from "../../@types/api"; +import { retry } from "../../utils/retry"; +import { + GetBuildingsResponse, + GetPopularResponse, + GetRouteResponse, +} from "../../@types/api"; import useNavigationProgress, { OffRouteCallbackPayload, } from "../../hooks/useNavigationProgress"; import { mockClosedTimes, mockOpenTimes } from "../../mock/buildings"; import { useTheme } from "styled-components/native"; import UserLocationIndicator from "../../components/UserLocationIndicator"; +import Banner from "../../components/Banner"; +import { Context } from "../../App"; SplashScreen.preventAutoHideAsync(); Mapbox.setAccessToken(MAPBOX_ACCESS_TOKEN); @@ -39,12 +52,24 @@ const defaultCameraSettings: Mapbox.CameraStop = { centerCoordinate: [-93.23532984426897, 44.974795560478185], // centers on campus if no location }; +const TOAST_IDS = { + buildings: "buildings-error", + route: "route-error", + reroute: "reroute-error", + locationPermission: "location-permission", +} as const; + const Home = () => { + const { hideSplash } = useContext(Context); + const [offline, setOffline] = useState(false); + const [setupFailed, setSetupFailed] = useState(false); + const [retries, setRetries] = useState(0); const theme = useTheme(); const insets = useSafeAreaInsets(); const cameraRef = useRef(null); const latestQuery = useRef | null>(null); const isReroutingRef = useRef(false); + const destinationRef = useRef(null); // loading const [loading, setLoading] = useState(false); const [loadingProgress, setLoadingProgress] = useState(null); @@ -58,13 +83,23 @@ const Home = () => { null, ); const [heading, setHeading] = useState(null); + const [selectedMarkerId, setSelectedMarkerId] = useState< + number | string | null + >(null); // page state const [onRoute, setOnRoute] = useState(false); + const [centered, setCentered] = useState(true); const [followUserLocation, setFollowUserLocation] = useState(onRoute); const [destination, setDestination] = useState< (typeof buildings)[number] | null >(null); + const [popularDestinations, setPopularDestinations] = + useState([]); + + useEffect(() => { + destinationRef.current = destination?.id ?? null; + }, [destination]); // memoized functions /** @@ -114,6 +149,17 @@ const Home = () => { [setLoadingProgress], ); + const clearRouteData = useCallback(() => { + if (latestQuery.current) { + clearTimeout(latestQuery.current); + latestQuery.current = null; + } + setCurrentRoute(null); + setLoading(false); + setLoadingProgress(null); + setOnRoute(false); + }, []); + const handleOffRoute = useCallback( async ({ location: offRouteLocation }: OffRouteCallbackPayload) => { if (!destination) return; @@ -133,8 +179,13 @@ const Home = () => { offRouteLocation.coords.longitude, ); setCurrentRoute(data); + toast.dismiss(TOAST_IDS.reroute); } catch (e) { devLog("error re-routing: ", e); + toast.warning("Rerouting failed", { + id: TOAST_IDS.reroute, + description: "We'll keep your last directions for now.", + }); } finally { setLoadingProgress(null); setLoading(false); @@ -152,52 +203,107 @@ const Home = () => { ); const routeSegments = navigation.normalizedRoute?.segments ?? []; - useLayoutEffect(() => { - (async () => { - try { - // get buildings before first render - const data = await getBuildings(); - devLog(data); - setBuildings(data); - } catch (e) { - devLog("error calling api: ", e); - } - })(); + const setup = async () => { + if (buildings?.length && popularDestinations?.length) return; + try { + // get buildings and popular destinations before first render + const [buildingsData, popular] = await Promise.all([ + retry(() => getBuildings(), 3, 800), + retry(() => getPopular(), 3, 800), + ]); + // devLog(buildingsData); + setBuildings(buildingsData); + setPopularDestinations(popular); + setSetupFailed(false); + } catch (e) { + setRetries((prev) => prev + 1); + setSetupFailed(true); + devLog("error calling api: ", e); + // toast.error("Couldn't load buildings", { + // id: TOAST_IDS.buildings, + // description: "Check your connection and try again.", + // }); + } finally { + hideSplash(); + } + }; + + useEffect(() => { + let isMounted = true; + + const handleNetInfoUpdate = (state: NetInfoState) => { + // devLog("network state updated:", state); + setOffline((prevOffline) => { + if (state.isInternetReachable === false || state.isConnected === false) + return true; + if (state.isInternetReachable === true || state.isConnected === true) + return false; + return prevOffline; + }); + }; + + setup(); + + NetInfo.fetch() + .then((state) => { + if (isMounted) handleNetInfoUpdate(state); + }) + .catch((error) => devLog("netinfo fetch failed:", error)); + + const unsubscribe = NetInfo.addEventListener(handleNetInfoUpdate); + return () => { + isMounted = false; + unsubscribe(); + }; }, []); useEffect(() => { (async () => { - const { status } = await Location.requestForegroundPermissionsAsync(); - if (status !== "granted") { - console.warn("Permission to access location was denied"); - return; - } - Location.watchPositionAsync( - { - accuracy: Location.LocationAccuracy.BestForNavigation, - distanceInterval: 1, - timeInterval: 500, - }, - (loc) => { - // devLog("location updated: ", loc); - setLocation(loc); - }, - ); - Location.watchHeadingAsync((headingValue) => { - const { trueHeading } = headingValue; - setHeading( - Number.isFinite(trueHeading) && trueHeading >= 0 - ? trueHeading - : null, + try { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== "granted") { + toast.error("Location permission denied", { + id: TOAST_IDS.locationPermission, + description: "Enable location to get navigation directions.", + }); + return; + } + Location.watchPositionAsync( + { + accuracy: Location.LocationAccuracy.BestForNavigation, + distanceInterval: 1, + timeInterval: 500, + }, + (loc) => { + // devLog("location updated: ", loc); + setLocation(loc); + }, ); - }); + Location.watchHeadingAsync((headingValue) => { + const { trueHeading } = headingValue; + setHeading( + Number.isFinite(trueHeading) && trueHeading >= 0 + ? trueHeading + : null, + ); + }); + } catch (e) { + devLog("error requesting location permissions: ", e); + toast.error("Unable to access location", { + id: TOAST_IDS.locationPermission, + description: "Please try again or check location services.", + }); + } })(); }, []); useEffect(() => { if (!location?.coords) return; setLoadingProgress(null); - if (latestQuery.current) clearTimeout(latestQuery.current); + if (latestQuery.current) { + clearTimeout(latestQuery.current); + latestQuery.current = null; + } if (!destination) { cameraRef.current?.setCamera({ heading: 0, @@ -207,25 +313,43 @@ const Home = () => { setCurrentRoute(null); return; } else { + const targetDestination = destination; + const targetDestinationId = destination.id; setLoading(true); simulateLoadingProgress(0.5, 0.95); - latestQuery.current = setTimeout(async () => { + const timeoutId = setTimeout(async () => { try { const data = await getRoute( - destination.buildingName, + targetDestination.buildingName, location?.coords.latitude, location?.coords.longitude, ); + if (destinationRef.current !== targetDestinationId) return; setCurrentRoute(data); - devLog("route data: ", data); + toast.dismiss(TOAST_IDS.route); + // devLog("route data: ", data); } catch (e) { + if (destinationRef.current !== targetDestinationId) return; devLog("error getting route: ", e); + toast.error("Couldn't load route", { + id: TOAST_IDS.route, + description: "Please check your connection and try again.", + }); } finally { + if (destinationRef.current !== targetDestinationId) return; setLoadingProgress(1); setLoading(false); setLoadingProgress(null); + if (latestQuery.current === timeoutId) latestQuery.current = null; } }, 500); + latestQuery.current = timeoutId; + return () => { + if (latestQuery.current === timeoutId) { + clearTimeout(timeoutId); + latestQuery.current = null; + } + }; } }, [destination]); @@ -238,24 +362,59 @@ const Home = () => { } }, [onRoute, followUserLocation]); - const handleMapPressed = useCallback((feature: GeoJSON.Feature) => { - if (!__DEV__) return; - const coords = ( - feature.geometry as unknown as { coordinates: [number, number] } - ).coordinates; // ? this seems to be a typing mistake from Mapbox, since it does return the forced type - devLog(coords); - setLocation( - (prev) => - ({ - ...prev, - coords: { - ...prev?.coords, - longitude: coords[0], - latitude: coords[1], - }, - }) as Location.LocationObject, - ); - }, []); + const handleMapPressed = useCallback( + (feature: GeoJSON.Feature) => { + if (selectedMarkerId) { + if (!onRoute) { + setDestination(null); + setCurrentRoute(null); + } + setSelectedMarkerId(null); + } + if (!__DEV__) return; + const coords = ( + feature.geometry as unknown as { coordinates: [number, number] } + ).coordinates; // ? this seems to be a typing mistake from Mapbox, since it does return the forced type + devLog(coords); + setLocation( + (prev) => + ({ + ...prev, + coords: { + ...prev?.coords, + longitude: coords[0], + latitude: coords[1], + }, + }) as Location.LocationObject, + ); + }, + [onRoute, selectedMarkerId], + ); + + const handleMarkerSelected = useCallback( + (building: (typeof buildings)[number] | null) => { + if (building === null) { + clearRouteData(); + setDestination(null); + setSelectedMarkerId(null); + return; + } + + const isAlreadySelected = destination?.id === building.id; + + if (isAlreadySelected) { + clearRouteData(); + setDestination(null); + setSelectedMarkerId(null); + return; + } + + clearRouteData(); + setSelectedMarkerId(building.id); + adjustMapToRoute(building); + }, + [adjustMapToRoute, clearRouteData, destination], + ); const isDarkMode = theme.name === "dark"; const userLocationCoordinate = location?.coords @@ -265,15 +424,29 @@ const Home = () => { ]) : null; - const handleUserLocationUpdate = useCallback( - (_location: MapboxLocation) => { - // no-op – keeps the internal location manager running even when hidden - }, - [], - ); + const handleUserLocationUpdate = useCallback((_location: MapboxLocation) => { + // no-op – keeps the internal location manager running even when hidden + }, []); return ( + {!offline && setupFailed && ( + + )} + {offline && ( + + )} { /> ) : ( { - setDestination(buildings.find((x) => x.id === destination.id)!); + const nextDestination = buildings.find( + (x) => x.id === destination.id, + ); + if (!nextDestination) return; + clearRouteData(); + setSelectedMarkerId(nextDestination.id); + adjustMapToRoute(nextDestination); }} /> )} { - if (!onRoute && location?.coords) - cameraRef.current?.moveTo( - [location?.coords.longitude, location?.coords.latitude], - 500, - ); - else if (onRoute && !followUserLocation) + if (!onRoute && location?.coords) { + cameraRef.current?.setCamera({ + centerCoordinate: [ + location?.coords.longitude, + location?.coords.latitude, + ], + zoomLevel: 17, + animationDuration: 500, + }); + setCentered(true); + } else if (onRoute && !followUserLocation) setFollowUserLocation(true); }} - centerButtonActive={onRoute && followUserLocation} + centerButtonActive={onRoute ? followUserLocation : centered} loading={loading} loadingProgress={loadingProgress || undefined} destinationInfo={{ @@ -338,13 +523,17 @@ const Home = () => { { - if (state.gestures.isGestureActive && followUserLocation) - setFollowUserLocation(false); + if (state.gestures.isGestureActive) { + if (followUserLocation) setFollowUserLocation(false); + if (centered) setCentered(false); + } }} > { userLocation={location?.coords ?? null} /> )} - {buildings - .filter((building) => { - if (!onRoute) return true; - if (!destination) return false; - return building.buildingName === destination.buildingName; - }) - .map((building, index) => ( - { - if (!onRoute) adjustMapToRoute(building); - }} - onDeselected={() => { - if (!onRoute) { - setDestination(null); - setCurrentRoute(null); - } - }} - /> - ))} + {buildings.map((building, index) => ( + {if (!onRoute) handleMarkerSelected(building)}} + /> + ))} ); diff --git a/assets/GopherTunnels-iOS-Default-1024x1024@1x.png b/assets/GopherTunnels-iOS-Default-1024x1024@1x.png deleted file mode 100644 index 4495362..0000000 Binary files a/assets/GopherTunnels-iOS-Default-1024x1024@1x.png and /dev/null differ diff --git a/assets/GopherTunnels-watchOS-Default-1088x1088@1x.png b/assets/GopherTunnels-watchOS-Default-1088x1088@1x.png deleted file mode 100644 index 7f287e1..0000000 Binary files a/assets/GopherTunnels-watchOS-Default-1088x1088@1x.png and /dev/null differ diff --git a/assets/GopherTunnels.icon/Assets/Gopher.svg b/assets/GopherTunnels.icon/Assets/Gopher.svg deleted file mode 100644 index 91b5710..0000000 --- a/assets/GopherTunnels.icon/Assets/Gopher.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/assets/GopherTunnels.icon/Assets/Marker.svg b/assets/GopherTunnels.icon/Assets/Marker.svg deleted file mode 100644 index f3c719e..0000000 --- a/assets/GopherTunnels.icon/Assets/Marker.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/GopherTunnels.icon/Assets/Tunnel.svg b/assets/GopherTunnels.icon/Assets/Tunnel.svg deleted file mode 100644 index 8065305..0000000 --- a/assets/GopherTunnels.icon/Assets/Tunnel.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png deleted file mode 100644 index f9bd7cf..0000000 Binary files a/assets/adaptive-icon.png and /dev/null differ diff --git a/assets/icon.png b/assets/icon.png deleted file mode 100644 index bec9f6f..0000000 Binary files a/assets/icon.png and /dev/null differ diff --git a/assets/iconDark.png b/assets/iconDark.png deleted file mode 100644 index 4a6197f..0000000 Binary files a/assets/iconDark.png and /dev/null differ diff --git a/assets/images/GopherTunnels-iOS-Default-1024x1024@2x.png b/assets/images/GopherTunnels-iOS-Default-1024x1024@2x.png new file mode 100644 index 0000000..ddafb49 Binary files /dev/null and b/assets/images/GopherTunnels-iOS-Default-1024x1024@2x.png differ diff --git a/assets/images/GopherTunnels.icon/Assets/Gopher.svg b/assets/images/GopherTunnels.icon/Assets/Gopher.svg new file mode 100644 index 0000000..177bb9e --- /dev/null +++ b/assets/images/GopherTunnels.icon/Assets/Gopher.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/GopherTunnels.icon/Assets/Marker.svg b/assets/images/GopherTunnels.icon/Assets/Marker.svg new file mode 100644 index 0000000..b553445 --- /dev/null +++ b/assets/images/GopherTunnels.icon/Assets/Marker.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/GopherTunnels.icon/Assets/bg.svg b/assets/images/GopherTunnels.icon/Assets/bg.svg new file mode 100644 index 0000000..0ac33aa --- /dev/null +++ b/assets/images/GopherTunnels.icon/Assets/bg.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/GopherTunnels.icon/Assets/tunnel.svg b/assets/images/GopherTunnels.icon/Assets/tunnel.svg new file mode 100644 index 0000000..2baa458 --- /dev/null +++ b/assets/images/GopherTunnels.icon/Assets/tunnel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/GopherTunnels.icon/icon.json b/assets/images/GopherTunnels.icon/icon.json similarity index 83% rename from assets/GopherTunnels.icon/icon.json rename to assets/images/GopherTunnels.icon/icon.json index 3aac45c..6c9102d 100644 --- a/assets/GopherTunnels.icon/icon.json +++ b/assets/images/GopherTunnels.icon/icon.json @@ -29,16 +29,13 @@ "hidden" : false, "layers" : [ { - "blend-mode" : "normal", - "glass" : true, - "hidden" : false, "image-name" : "Gopher.svg", "name" : "Gopher", "position" : { "scale" : 1, "translation-in-points" : [ - 0, - -117 + 3.0999348155091866e-07, + -116.09948868192862 ] } } @@ -96,13 +93,14 @@ ], "layers" : [ { - "image-name" : "Tunnel.svg", - "name" : "Tunnel", + "hidden" : false, + "image-name" : "tunnel.svg", + "name" : "tunnel", "position" : { "scale" : 1, "translation-in-points" : [ - 0, - 56 + 3.0999348155091866e-07, + 63.01757239263782 ] } } @@ -134,7 +132,7 @@ { "value" : { "enabled" : true, - "value" : 0.75 + "value" : 0.9 } }, { @@ -146,6 +144,32 @@ } ] }, + { + "blur-material" : null, + "layers" : [ + { + "glass" : true, + "hidden" : false, + "image-name" : "bg.svg", + "name" : "bg", + "position" : { + "scale" : 1.63, + "translation-in-points" : [ + 0, + -59 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.1 + } + }, { "blur-material-specializations" : [ { @@ -164,11 +188,12 @@ "scale" : 1, "translation-in-points" : [ 0, - 0 + 15.998547198770268 ] } } ], + "lighting" : "individual", "name" : "Group", "position" : { "scale" : 1.63, diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png new file mode 100644 index 0000000..2f92cef Binary files /dev/null and b/assets/images/adaptive-icon.png differ diff --git a/assets/customMarker.svg b/assets/images/customMarker.svg similarity index 100% rename from assets/customMarker.svg rename to assets/images/customMarker.svg diff --git a/assets/favicon.png b/assets/images/favicon.png similarity index 100% rename from assets/favicon.png rename to assets/images/favicon.png diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..0e3e962 Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/images/iconDark.png b/assets/images/iconDark.png new file mode 100644 index 0000000..ab728b4 Binary files /dev/null and b/assets/images/iconDark.png differ diff --git a/assets/m-house.png b/assets/images/m-house.png similarity index 100% rename from assets/m-house.png rename to assets/images/m-house.png diff --git a/assets/magnifying-glass.png b/assets/images/magnifying-glass.png similarity index 100% rename from assets/magnifying-glass.png rename to assets/images/magnifying-glass.png diff --git a/assets/images/splashDark.png b/assets/images/splashDark.png new file mode 100644 index 0000000..2cdc161 Binary files /dev/null and b/assets/images/splashDark.png differ diff --git a/assets/images/splashIcon.svg b/assets/images/splashIcon.svg new file mode 100644 index 0000000..947e849 --- /dev/null +++ b/assets/images/splashIcon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/splashLight.png b/assets/images/splashLight.png new file mode 100644 index 0000000..7fd52e3 Binary files /dev/null and b/assets/images/splashLight.png differ diff --git a/assets/userButton.svg b/assets/images/userButton.svg similarity index 100% rename from assets/userButton.svg rename to assets/images/userButton.svg diff --git a/assets/lottie/animated-logo-in.json b/assets/lottie/animated-logo-in.json new file mode 100644 index 0000000..b52759f --- /dev/null +++ b/assets/lottie/animated-logo-in.json @@ -0,0 +1 @@ +{"assets":[{"id":"7","layers":[{"ind":6,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[16.5,19.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[33,39]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[1.06,-0.91],[1.45,0.11],[0.91,1.06],[-0.17,1.39],[-0.09,0.52],[-8.17,3.26],[-0.46,0.18],[-0.98,-0.42],[-0.41,-0.96],[0.38,-1],[0.95,-0.45],[0.33,-0.18],[0.48,-6.02],[0,-0.31]],"o":[[-0.05,1.39],[-1.05,0.91],[-1.45,-0.11],[-0.91,-1.06],[0.06,-0.54],[1.56,-9.56],[0.45,-0.19],[0.98,-0.37],[0.98,0.42],[0.41,0.96],[-0.38,1],[-0.35,0.17],[-6,3.12],[-0.03,0.32],[0,0]],"v":[[10.51,23.95],[8.8,27.54],[4.87,28.78],[1.17,26.95],[0.04,23.14],[0.27,21.53],[17.3,0.8],[18.66,0.25],[21.73,0.35],[23.91,2.5],[23.95,5.56],[21.89,7.84],[20.88,8.36],[10.55,22.99],[10.51,23.95]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"10","layers":[{"ind":9,"ty":0,"parent":5,"ks":{},"w":33,"h":39,"ip":0,"op":131.8,"st":0,"refId":"7"},{"ind":5,"ty":3,"ks":{"s":{"a":0,"k":[197.54,202.52]}},"ip":0,"op":131.8,"st":0}]},{"id":"16","layers":[{"ind":15,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[16.5,19.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[33,39]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-1.06,-0.91],[-1.45,0.11],[-0.9,1.06],[0.17,1.39],[0.09,0.52],[8.17,3.26],[0.46,0.18],[0.98,-0.42],[0.41,-0.96],[-0.38,-1],[-0.95,-0.45],[-0.33,-0.18],[-0.48,-6.02],[0,-0.31]],"o":[[0.05,1.39],[1.06,0.91],[1.45,-0.11],[0.91,-1.06],[-0.06,-0.54],[-1.56,-9.56],[-0.45,-0.19],[-0.98,-0.37],[-0.98,0.42],[-0.41,0.96],[0.38,1],[0.35,0.17],[6,3.12],[0.03,0.32],[0,0]],"v":[[13.72,23.95],[15.42,27.54],[19.36,28.78],[23.05,26.95],[24.19,23.14],[23.95,21.53],[6.93,0.8],[5.57,0.25],[2.49,0.35],[0.32,2.5],[0.27,5.56],[2.34,7.84],[3.35,8.36],[13.67,22.99],[13.72,23.95]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"19","layers":[{"ind":18,"ty":0,"parent":14,"ks":{},"w":33,"h":39,"ip":0,"op":131.8,"st":0,"refId":"16"},{"ind":14,"ty":3,"ks":{"s":{"a":0,"k":[203.53,202.52]}},"ip":0,"op":131.8,"st":0}]},{"id":"24","layers":[{"ind":23,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[38.7,0],[0,30.5],[-38.7,0],[0,-34.6]],"v":[[140.3,43.4],[70.1,98.7],[0,43.4],[70.1,0],[140.3,43.4]],"o":[[0,30.5],[-38.7,0],[0,-34.6],[38.7,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]}]},{"id":"30","layers":[{"ind":29,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[83.5,25]},"r":{"a":0,"k":0},"s":{"a":0,"k":[167,50]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[20,0],[0,28.5]],"o":[[-4.5,8.17],[-25,0],[0,0]],"v":[[161,20.5],[123.5,44],[83.5,7.5]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[25,0],[4.5,8.17]],"o":[[0,28.5],[-20,0],[0,0]],"v":[[83.5,7.5],[43.5,44],[6,20.5]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[83.5,7.5],[83.5,6]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":12}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"33","layers":[{"ind":32,"ty":0,"parent":28,"ks":{},"w":167,"h":50,"ip":0,"op":131.8,"st":0,"refId":"30"},{"ind":28,"ty":3,"ks":{"s":{"a":0,"k":[199.91,201.41]}},"ip":0,"op":131.8,"st":0}]},{"id":"40","layers":[{"ind":39,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[15.5,19]},"r":{"a":0,"k":0},"s":{"a":0,"k":[31,38]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-9,0],[0,0],[-0.07,4.92],[0,0]],"o":[[1,12.67],[0,0],[4.92,0],[0,0],[0,0]],"v":[[0,0],[9,38],[21.62,38],[30.62,29.12],[31,0]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":1}},{"ty":"fl","c":{"a":0,"k":[1,0.91,0.65,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"43","layers":[{"ind":42,"ty":0,"parent":38,"ks":{},"w":31,"h":38,"ip":0,"op":131.8,"st":0,"refId":"40"},{"ind":38,"ty":3,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":131.8,"st":0}]},{"id":"49","layers":[{"ind":48,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[15.5,19]},"r":{"a":0,"k":0},"s":{"a":0,"k":[31,38]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[9,0],[0,0],[0.07,4.92],[0,0]],"o":[[-1,12.67],[0,0],[-4.92,0],[0,0],[0,0]],"v":[[31,0],[22,38],[9.38,38],[0.38,29.12],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":1}},{"ty":"fl","c":{"a":0,"k":[1,0.91,0.65,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"52","layers":[{"ind":51,"ty":0,"parent":47,"ks":{},"w":31,"h":38,"ip":0,"op":131.8,"st":0,"refId":"49"},{"ind":47,"ty":3,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":131.8,"st":0}]},{"id":"58","layers":[{"ind":57,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[49,25]},"r":{"a":0,"k":0},"s":{"a":0,"k":[98,50]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[5.77,0],[0,0],[1.07,5.67]],"o":[[0,0],[0,0],[0,0],[-1.07,5.67],[0,0],[-5.77,0],[0,0]],"v":[[12.99,33.73],[7.84,6.5],[89.34,6.5],[84.19,33.73],[72.4,43.5],[24.79,43.5],[12.99,33.73]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":13}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[4.33,0],[0,0],[0.8,4.25]],"o":[[0,0],[0,0],[0,0],[-0.8,4.25],[0,0],[-4.32,0],[0,0]],"v":[[9.75,25.3],[5.88,4.88],[67.01,4.88],[63.14,25.3],[54.3,32.63],[18.59,32.63],[9.75,25.3]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"61","layers":[{"ind":60,"ty":0,"parent":56,"ks":{},"w":98,"h":50,"ip":0,"op":131.8,"st":0,"refId":"58"},{"ind":56,"ty":3,"ks":{"s":{"a":0,"k":[197.54,201.41]}},"ip":0,"op":131.8,"st":0}]},{"id":"67","layers":[{"ind":66,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[40.5,36.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[81,73]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-18.75,0],[25.5,0],[-0.62,9],[0,21.38]],"o":[[0,21.38],[0.63,9],[-25.5,0],[18.75,0],[0,0]],"v":[[30.05,0],[60.05,27.38],[30.05,54.38],[0.05,27.38],[30.05,0]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"70","layers":[{"ind":69,"ty":0,"parent":65,"ks":{},"w":81,"h":73,"ip":0,"op":131.8,"st":0,"refId":"67"},{"ind":65,"ty":3,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":131.8,"st":0}]},{"id":"79","layers":[{"ind":76,"ty":4,"parent":75,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[6.914,6.911]},"s":{"a":0,"k":[13.828,13.822]}},{"ty":"fl","c":{"a":0,"k":[1,0.914,0.655]},"o":{"a":0,"k":100}}]},{"ind":75,"ty":3,"ks":{"p":{"a":0,"k":[27.656,21.72]}},"ip":0,"op":131.8,"st":0},{"ind":78,"ty":4,"parent":77,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[34.57,41.466]},"s":{"a":0,"k":[69.14,82.933]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":77,"ty":3,"ks":{},"ip":0,"op":131.8,"st":0}]},{"id":"87","layers":[{"ind":84,"ty":4,"parent":83,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[6.914,6.911]},"s":{"a":0,"k":[13.828,13.822]}},{"ty":"fl","c":{"a":0,"k":[1,0.914,0.655]},"o":{"a":0,"k":100}}]},{"ind":83,"ty":3,"ks":{"p":{"a":0,"k":[27.656,21.72]}},"ip":0,"op":131.8,"st":0},{"ind":86,"ty":4,"parent":85,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[34.57,41.466]},"s":{"a":0,"k":[69.14,82.933]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":85,"ty":3,"ks":{},"ip":0,"op":131.8,"st":0}]},{"id":"93","layers":[{"ind":92,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[80.5,36.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[161,73]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-3.37,6.13],[0,0],[30,9.9],[11.25,-0.37],[0,0],[-18.75,0]],"o":[[15,0],[0,0],[0,-4.12],[0,0],[0,0],[0,21.38],[0,0]],"v":[[90.38,54.43],[118.5,36.81],[120.75,33.43],[86.63,3.06],[60.38,0.06],[60.38,27.06],[90.38,54.43]]}}},{"ty":"fl","c":{"a":0,"k":[0.76,0.56,0.37,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[3.38,6.13],[0,0],[-30,9.9],[-11.25,-0.37],[0,-21.37],[18.75,0]],"o":[[-15,0],[0,0],[0,-4.12],[0,0],[0,0],[0,21.38],[0,0]],"v":[[30.38,54.4],[2.25,36.78],[0,33.4],[34.13,3.03],[60.38,0.03],[60.38,27.03],[30.38,54.4]]}}},{"ty":"fl","c":{"a":0,"k":[0.76,0.56,0.37,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"96","layers":[{"ind":95,"ty":0,"parent":91,"ks":{},"w":161,"h":73,"ip":0,"op":131.8,"st":0,"refId":"93"},{"ind":91,"ty":3,"ks":{"s":{"a":0,"k":[201.86,205.48]}},"ip":0,"op":131.8,"st":0}]},{"id":"102","layers":[{"ind":101,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[156.5,134.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[313,269]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-13.54,-13.54],[13.9,-13.9],[4.9,-2.11],[0,-16.72],[0,0],[-0.1,-0.17],[0,-20.56],[0,0],[24.84,-16.35],[49.62,1.28],[24.92,15.73],[0,25.73],[0,0],[-9.59,15.81],[0,0.38],[0,0.05],[-8.76,13.88],[4.09,4.09],[-13.54,13.54],[0,0],[-13.89,-13.9],[-2.05,-5.84],[-28.73,0],[-21.86,-12.14],[-4.77,4.77]],"o":[[13.89,-13.9],[13.54,13.54],[-4.07,4.07],[8.77,13.89],[0,0],[0,0.38],[9.74,16.06],[0,0],[-0.39,25.15],[-25.03,16.47],[-49.4,-1.27],[-25.22,-15.92],[0,0],[0.19,-19.99],[0.1,-0.17],[0,-0.05],[0,-16.71],[-4.92,-2.11],[-13.9,-13.9],[0,0],[13.54,-13.54],[4.78,4.78],[21.89,-12.17],[28.7,0],[2.06,-5.82],[0,0]],"v":[[244.05,19.54],[293.9,15.49],[289.85,65.34],[276.22,74.64],[290,120.99],[290,121.15],[290.19,121.96],[306,170.99],[305.99,172.19],[268.16,236.82],[156.32,261.48],[44.95,235.01],[7,170.99],[7.01,170.03],[22.81,121.96],[23,121.15],[23,120.99],[36.75,74.68],[23.05,65.34],[19,15.49],[19.01,15.49],[68.85,19.54],[79.16,35.75],[156.5,16.49],[233.74,35.7],[244.05,19.54]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":14}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-10.16,-10.16],[10.42,-10.42],[3.67,-1.58],[0,-12.54],[0,0],[-0.07,-0.12],[0,-15.42],[0,0],[18.63,-12.26],[37.21,0.96],[18.69,11.8],[0,19.3],[0,0],[-7.19,11.86],[0,0.28],[0,0.04],[-6.57,10.41],[3.07,3.07],[-10.15,10.16],[0,0],[-10.42,-10.42],[-1.54,-4.38],[-21.55,0],[-16.4,-9.11],[-3.58,3.57]],"o":[[10.42,-10.42],[10.15,10.16],[-3.05,3.05],[6.58,10.41],[0,0],[0,0.28],[7.3,12.05],[0,0],[-0.29,18.86],[-18.77,12.35],[-37.05,-0.95],[-18.92,-11.94],[0,0],[0.14,-15],[0.07,-0.12],[0,-0.04],[0,-12.53],[-3.69,-1.58],[-10.42,-10.42],[0,0],[10.15,-10.16],[3.59,3.59],[16.41,-9.13],[21.52,0],[1.54,-4.37],[0,0]],"v":[[183.04,14.65],[220.42,11.62],[217.39,49],[207.17,55.98],[217.5,90.74],[217.5,90.86],[217.64,91.47],[229.5,128.24],[229.49,129.14],[201.12,177.61],[117.24,196.11],[33.71,176.26],[5.25,128.24],[5.25,127.52],[17.11,91.47],[17.25,90.86],[17.25,90.74],[27.56,56.01],[17.29,49],[14.25,11.62],[14.25,11.62],[51.64,14.65],[59.37,26.81],[117.38,12.37],[175.31,26.78],[183.04,14.65]]}}},{"ty":"fl","c":{"a":0,"k":[0.69,0.46,0.28,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"105","layers":[{"ind":104,"ty":0,"parent":100,"ks":{},"w":313,"h":269,"ip":0,"op":131.8,"st":0,"refId":"102"},{"ind":100,"ty":3,"ks":{"s":{"a":0,"k":[197.54,198.19]}},"ip":0,"op":131.8,"st":0}]},{"id":"117","layers":[{"ind":116,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[52.5,63]},"r":{"a":0,"k":0},"s":{"a":0,"k":[105,126]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-20.71],[0,0],[20.71,0],[0,0],[0,20.71],[0,0],[-20.71,0]],"o":[[20.71,0],[0,0],[0,20.71],[0,0],[-20.71,0],[0,0],[0,-20.71],[0,0]],"v":[[41.25,0],[78.75,37.5],[78.75,57],[41.25,94.5],[37.5,94.5],[0,57],[0,37.5],[37.5,0]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-12.22],[0,0],[-12.22,0],[0,12.22],[0,0],[12.22,0]],"o":[[-12.22,0],[0,0],[0,12.22],[12.22,0],[0,0],[0,-12.22],[0,0]],"v":[[39.38,18.75],[17.25,40.88],[17.25,53.63],[39.38,75.75],[61.5,53.63],[61.5,40.88],[39.38,18.75]]}}},{"ty":"fl","c":{"a":0,"k":[1,0.8,0.2,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"120","layers":[{"ind":119,"ty":0,"parent":115,"ks":{},"w":105,"h":126,"ip":0,"op":131.8,"st":0,"refId":"117"},{"ind":115,"ty":3,"ks":{"s":{"a":0,"k":[197.38,197.64]}},"ip":0,"op":131.8,"st":0}]},{"id":"124","layers":[{"ind":122,"ty":0,"ks":{"p":{"a":0,"k":[20.005,20.005]}},"w":208,"h":250,"ip":0,"op":131.8,"st":0,"refId":"120"},{"ind":123,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[123.628,144.519]},"r":{"a":0,"k":0},"s":{"a":0,"k":[247.257,289.037]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]}]},{"id":"127","layers":[{"ind":126,"ty":0,"parent":114,"ks":{"p":{"a":0,"k":[-20.005,-20.005]}},"ef":[{"ty":29,"ef":[{"ty":0,"nm":"","v":{"a":1,"k":[{"t":0,"s":[25.64],"h":1},{"t":37.8,"s":[25.64],"i":{"x":0.435,"y":0.615},"o":{"x":0.073,"y":0.141}},{"t":42,"s":[33.34],"i":{"x":0.365,"y":0.644},"o":{"x":0.035,"y":0.296}},{"t":42.858,"s":[28.53],"i":{"x":0.511,"y":0.574},"o":{"x":0.201,"y":0.238}},{"t":43.71,"s":[26.26],"i":{"x":0.545,"y":0.593},"o":{"x":0.23,"y":0.267}},{"t":45.42,"s":[22.84],"i":{"x":0.487,"y":0.537},"o":{"x":0.152,"y":0.172}},{"t":48.84,"s":[17.43],"i":{"x":0.379,"y":0.943},"o":{"x":0.146,"y":0.443}},{"t":85.8,"s":[0.09],"i":{"x":0.621,"y":1},"o":{"x":0.287,"y":0.586}},{"t":90,"s":[0],"h":1},{"t":130.8,"s":[0],"h":1}]}},{"ty":7,"nm":"","v":{"a":0,"k":1}},{"ty":7,"nm":"","v":{"a":0,"k":0}}]}],"w":247.2568,"h":289.0374,"ip":0,"op":131.8,"st":0,"refId":"124"},{"ind":114,"ty":3,"ks":{"p":{"a":0,"k":[21,21]}},"ip":0,"op":131.8,"st":0}]},{"id":"132","layers":[{"ind":112,"ty":4,"td":1,"parent":110,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[301,301.401]},"s":{"a":0,"k":[602,602.802]}},{"ty":"fl","c":{"a":0,"k":[0,0,0]},"o":{"a":0,"k":100}}]},{"ind":111,"ty":4,"tt":1,"parent":110,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[301,301.401]},"r":{"a":0,"k":0},"s":{"a":0,"k":[652,652.802]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[301,301.401]},"s":{"a":0,"k":[602,602.802]}},{"ty":"st","c":{"a":0,"k":[0.341,0,0.071]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":50}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":110,"ty":3,"parent":109,"ks":{},"ip":0,"op":131.8,"st":0},{"ind":129,"ty":0,"parent":113,"ks":{"a":{"a":0,"k":[21,21]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":42,"s":[0],"i":{"x":0,"y":1},"o":{"x":0,"y":0}},{"t":90,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,103.623],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":42,"s":[0,103.623],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":90,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[0,0],"h":1}]}},"w":249,"h":291,"ip":0,"op":131.8,"st":0,"refId":"127"},{"ind":113,"ty":3,"parent":109,"ks":{"p":{"a":0,"k":[197.377,420.973]}},"ip":0,"op":131.8,"st":0},{"ind":131,"ty":4,"parent":130,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[301,301.401]},"s":{"a":0,"k":[602,602.802]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":130,"ty":3,"parent":109,"ks":{},"ip":0,"op":131.8,"st":0},{"ind":109,"ty":3,"ks":{"p":{"a":0,"k":[21,21]}},"ip":0,"op":131.8,"st":0}]},{"id":"139","layers":[{"ind":138,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[191.5,237]},"r":{"a":0,"k":0},"s":{"a":0,"k":[383,474]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-101.34],[14.68,-27.74],[20.38,-26.37],[11.11,-11.07],[4.66,4.65],[40.77,52.77],[14.78,27.93],[0,24.43],[-101.34,0]],"o":[[101.34,0],[0,24.43],[-14.78,27.93],[-40.77,52.77],[-4.66,4.65],[-11.11,-11.07],[-20.37,-26.37],[-14.68,-27.74],[0,-101.34],[0,0]],"v":[[191.5,8],[375,191.5],[350.17,271.52],[295.18,354.53],[199.82,462.52],[183.18,462.52],[87.82,354.53],[32.83,271.52],[8,191.5],[191.5,8]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-76.01],[11.01,-20.81],[15.28,-19.78],[8.34,-8.3],[3.49,3.48],[30.57,39.57],[11.08,20.95],[0,18.32],[-76.01,0]],"o":[[76.01,0],[0,18.32],[-11.08,20.95],[-30.57,39.57],[-3.49,3.48],[-8.34,-8.3],[-15.28,-19.78],[-11.01,-20.81],[0,-76.01],[0,0]],"v":[[143.63,6],[281.25,143.63],[262.63,203.64],[221.39,265.89],[149.86,346.89],[137.39,346.89],[65.86,265.89],[24.63,203.64],[6,143.63],[143.63,6]]}}},{"ty":"fl","c":{"a":0,"k":[1,0.8,0.2,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"142","layers":[{"ind":141,"ty":0,"parent":137,"ks":{},"w":383,"h":474,"ip":0,"op":131.8,"st":0,"refId":"139"},{"ind":137,"ty":3,"ks":{"s":{"a":0,"k":[197.39,197.89]}},"ip":0,"op":131.8,"st":0}]},{"id":"146","layers":[{"ind":144,"ty":0,"ks":{"p":{"a":0,"k":[20,20]}},"w":756,"h":938,"ip":0,"op":131.8,"st":0,"refId":"142"},{"ind":145,"ty":4,"ks":{},"ip":0,"op":131.8,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[398,489]},"r":{"a":0,"k":0},"s":{"a":0,"k":[796,978]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]}]},{"id":"149","layers":[{"ind":148,"ty":0,"parent":136,"ks":{"p":{"a":0,"k":[-20,-20]}},"ef":[{"ty":29,"ef":[{"ty":0,"nm":"","v":{"a":1,"k":[{"t":0,"s":[33.33],"i":{"x":0.174,"y":0.759},"o":{"x":0.011,"y":0.227}},{"t":24,"s":[3.67],"i":{"x":0.722,"y":1},"o":{"x":0.35,"y":0.828}},{"t":48,"s":[0],"h":1},{"t":130.8,"s":[0],"h":1}]}},{"ty":7,"nm":"","v":{"a":0,"k":1}},{"ty":7,"nm":"","v":{"a":0,"k":0}}]}],"w":796,"h":978,"ip":0,"op":131.8,"st":0,"refId":"146"},{"ind":136,"ty":3,"ks":{"p":{"a":0,"k":[21,21]}},"ip":0,"op":131.8,"st":0}]}],"fr":60,"h":2622,"ip":0,"layers":[{"ind":12,"ty":0,"parent":4,"ks":{"a":{"a":0,"k":[32.595,39.492]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":102,"s":[0],"h":1},{"t":102,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[32.595,39.492]},"s":{"a":1,"k":[{"t":0,"s":[70,70],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":102,"s":[70,70],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":108.132,"s":[105.1,105.1],"i":{"x":[0.26,0.26],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]}},{"t":123,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":66,"h":79,"ip":0,"op":131.8,"st":0,"refId":"10"},{"ind":4,"ty":3,"parent":3,"ks":{"p":{"a":0,"k":[424.719,-11.848]}},"ip":0,"op":131.8,"st":0},{"ind":21,"ty":0,"parent":13,"ks":{"a":{"a":0,"k":[33.582,39.492]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":100.2,"s":[0],"h":1},{"t":100.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[33.582,39.492]},"s":{"a":1,"k":[{"t":0,"s":[70,70],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":100.2,"s":[70,70],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":106.332,"s":[105.1,105.1],"i":{"x":[0.26,0.26],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]}},{"t":121.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":68,"h":79,"ip":0,"op":131.8,"st":0,"refId":"19"},{"ind":13,"ty":3,"parent":3,"ks":{"p":{"a":0,"k":[-11.853,-11.848]}},"ip":0,"op":131.8,"st":0},{"ind":3,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[41.484,30.497]}},"ip":0,"op":131.8,"st":0},{"ind":26,"ty":0,"parent":22,"ks":{"a":{"a":0,"k":[70.128,49.365]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":82.2,"s":[0],"h":1},{"t":82.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[70.128,49.365]},"s":{"a":1,"k":[{"t":0,"s":[70,70],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":82.2,"s":[70,70],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":89.208,"s":[105.1,105.1],"i":{"x":[0.26,0.26],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]}},{"t":106.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":141,"h":99,"ip":0,"op":131.8,"st":0,"refId":"24"},{"ind":22,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[211.372,184.515]}},"ip":0,"op":131.8,"st":0},{"ind":35,"ty":0,"parent":27,"ks":{"a":{"a":0,"k":[166.925,50.352]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":85.2,"s":[0],"i":{"x":0,"y":1},"o":{"x":0,"y":0}},{"t":109.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[166.925,50.352]},"s":{"a":1,"k":[{"t":0,"s":[50,50],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":85.2,"s":[50,50],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":109.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":334,"h":101,"ip":0,"op":131.8,"st":0,"refId":"33"},{"ind":27,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[114.576,269.422]}},"ip":0,"op":131.8,"st":0},{"ind":45,"ty":0,"parent":37,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":92.4,"s":[0],"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"t":116.4,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,15.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":92.4,"s":[0,15.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":116.4,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[0,0],"h":1}]}},"w":62,"h":76,"ip":0,"op":131.8,"st":0,"refId":"43"},{"ind":37,"ty":3,"parent":36,"ks":{},"ip":0,"op":131.8,"st":0},{"ind":54,"ty":0,"parent":46,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":92.4,"s":[0],"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"t":116.4,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,15.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":92.4,"s":[0,15.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":116.4,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[0,0],"h":1}]}},"w":62,"h":76,"ip":0,"op":131.8,"st":0,"refId":"52"},{"ind":46,"ty":3,"parent":36,"ks":{"p":{"a":0,"k":[79.017,0]}},"ip":0,"op":131.8,"st":0},{"ind":36,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[211.372,348.406]}},"ip":0,"op":131.8,"st":0},{"ind":63,"ty":0,"parent":55,"ks":{"a":{"a":0,"k":[96.796,50.352]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":85.2,"s":[0],"i":{"x":0,"y":1},"o":{"x":0,"y":0}},{"t":109.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[96.796,50.352]},"s":{"a":1,"k":[{"t":0,"s":[50,50],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":85.2,"s":[50,50],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":109.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":194,"h":101,"ip":0,"op":131.8,"st":0,"refId":"61"},{"ind":55,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[185.691,344.456]}},"ip":0,"op":131.8,"st":0},{"ind":72,"ty":0,"parent":64,"ks":{"a":{"a":0,"k":[80.005,72.072]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":85.2,"s":[0],"i":{"x":0,"y":1},"o":{"x":0,"y":0}},{"t":109.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[80.005,72.072]},"s":{"a":1,"k":[{"t":0,"s":[50,50],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":85.2,"s":[50,50],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":109.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":161,"h":145,"ip":0,"op":131.8,"st":0,"refId":"70"},{"ind":64,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[201.495,285.219]}},"ip":0,"op":131.8,"st":0},{"ind":81,"ty":0,"parent":74,"ks":{"a":{"a":0,"k":[34.57,41.466]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":70.2,"s":[0],"h":1},{"t":70.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[34.57,41.466]},"s":{"a":1,"k":[{"t":0,"s":[120,120],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":70.2,"s":[120,120],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":77.1,"s":[92.8,92.8],"i":{"x":[0.24,0.24],"y":[1,1]},"o":{"x":[0.18,0.18],"y":[0,0]}},{"t":100.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":70,"h":83,"ip":0,"op":131.8,"st":0,"refId":"79"},{"ind":74,"ty":3,"parent":73,"ks":{"p":{"a":0,"k":[242.979,0]}},"ip":0,"op":131.8,"st":0},{"ind":89,"ty":0,"parent":82,"ks":{"a":{"a":0,"k":[34.57,41.466]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":70.2,"s":[0],"h":1},{"t":70.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[34.57,41.466]},"s":{"a":1,"k":[{"t":0,"s":[120,120],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":70.2,"s":[120,120],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":77.1,"s":[92.8,92.8],"i":{"x":[0.24,0.24],"y":[1,1]},"o":{"x":[0.18,0.18],"y":[0,0]}},{"t":100.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":70,"h":83,"ip":0,"op":131.8,"st":0,"refId":"87"},{"ind":82,"ty":3,"parent":73,"ks":{},"ip":0,"op":131.8,"st":0},{"ind":73,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[124.453,123.303]}},"ip":0,"op":131.8,"st":0},{"ind":98,"ty":0,"parent":90,"ks":{"a":{"a":0,"k":[162.5,75]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":76.2,"s":[0],"h":1},{"t":76.2,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[162.5,75]},"s":{"a":1,"k":[{"t":0,"s":[120,120],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":76.2,"s":[120,120],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":83.1,"s":[92.8,92.8],"i":{"x":[0.24,0.24],"y":[1,1]},"o":{"x":[0.18,0.18],"y":[0,0]}},{"t":106.2,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":325,"h":151,"ip":0,"op":131.8,"st":0,"refId":"96"},{"ind":90,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[119,210]}},"ip":0,"op":131.8,"st":0},{"ind":107,"ty":0,"parent":99,"ks":{"a":{"a":0,"k":[309.156,266.569]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":47.4,"s":[0],"h":1},{"t":47.4,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[309.156,266.569]},"s":{"a":1,"k":[{"t":0,"s":[70,70],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":47.4,"s":[70,70],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":59.664,"s":[105.1,105.1],"i":{"x":[0.26,0.26],"y":[1,1]},"o":{"x":[0.2,0.2],"y":[0,0]}},{"t":89.4,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":619,"h":534,"ip":0,"op":131.8,"st":0,"refId":"105"},{"ind":99,"ty":3,"parent":2,"ks":{"p":{"a":0,"k":[-27.656,-28.74]}},"ip":0,"op":131.8,"st":0},{"ind":2,"ty":3,"parent":1,"ks":{"p":{"a":0,"k":[324,832]}},"ip":0,"op":131.8,"st":0},{"ind":134,"ty":0,"parent":108,"ks":{"a":{"a":0,"k":[322,356]},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":37.8,"s":[0],"h":1},{"t":37.8,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":0,"k":[301,335]},"s":{"a":1,"k":[{"t":0,"s":[130,130],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[130,130],"i":{"x":[0.2,0.2],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":48.84,"s":[89.2,89.2],"i":{"x":[0.24,0.24],"y":[1,1]},"o":{"x":[0.18,0.18],"y":[0,0]}},{"t":85.8,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[100,100],"h":1}]}},"w":623,"h":815,"ip":0,"op":131.8,"st":0,"refId":"132"},{"ind":108,"ty":3,"parent":1,"ks":{"p":{"a":0,"k":[302,923]}},"ip":0,"op":131.8,"st":0},{"ind":151,"ty":0,"parent":135,"ks":{"a":{"a":0,"k":[21,21]},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0,"y":1},"o":{"x":0,"y":0}},{"t":48,"s":[100],"h":1},{"t":130.8,"s":[100],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,-378],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":48,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":130.8,"s":[0,0],"h":1}]}},"w":797,"h":979,"ip":0,"op":131.8,"st":0,"refId":"149"},{"ind":135,"ty":3,"parent":1,"ks":{"p":{"a":0,"k":[225,853]}},"ip":0,"op":131.8,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":131.8,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":131.8,"st":0}],"meta":{"g":"https://jitter.video"},"op":130.8,"v":"5.7.4","w":1206} \ No newline at end of file diff --git a/assets/lottie/animated-logo-out.json b/assets/lottie/animated-logo-out.json new file mode 100644 index 0000000..e070f61 --- /dev/null +++ b/assets/lottie/animated-logo-out.json @@ -0,0 +1 @@ +{"assets":[{"id":"9","layers":[{"ind":8,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[16.5,19.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[33,39]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[1.06,-0.91],[1.45,0.11],[0.91,1.06],[-0.17,1.39],[-0.09,0.52],[-8.17,3.26],[-0.46,0.18],[-0.98,-0.42],[-0.41,-0.96],[0.38,-1],[0.95,-0.45],[0.33,-0.18],[0.48,-6.02],[0,-0.31]],"o":[[-0.05,1.39],[-1.05,0.91],[-1.45,-0.11],[-0.91,-1.06],[0.06,-0.54],[1.56,-9.56],[0.45,-0.19],[0.98,-0.37],[0.98,0.42],[0.41,0.96],[-0.38,1],[-0.35,0.17],[-6,3.12],[-0.03,0.32],[0,0]],"v":[[10.51,23.95],[8.8,27.54],[4.87,28.78],[1.17,26.95],[0.04,23.14],[0.27,21.53],[17.3,0.8],[18.66,0.25],[21.73,0.35],[23.91,2.5],[23.95,5.56],[21.89,7.84],[20.88,8.36],[10.55,22.99],[10.51,23.95]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"15","layers":[{"ind":14,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[16.5,19.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[33,39]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-1.06,-0.91],[-1.45,0.11],[-0.9,1.06],[0.17,1.39],[0.09,0.52],[8.17,3.26],[0.46,0.18],[0.98,-0.42],[0.41,-0.96],[-0.38,-1],[-0.95,-0.45],[-0.33,-0.18],[-0.48,-6.02],[0,-0.31]],"o":[[0.05,1.39],[1.06,0.91],[1.45,-0.11],[0.91,-1.06],[-0.06,-0.54],[-1.56,-9.56],[-0.45,-0.19],[-0.98,-0.37],[-0.98,0.42],[-0.41,0.96],[0.38,1],[0.35,0.17],[6,3.12],[0.03,0.32],[0,0]],"v":[[13.72,23.95],[15.42,27.54],[19.36,28.78],[23.05,26.95],[24.19,23.14],[23.95,21.53],[6.93,0.8],[5.57,0.25],[2.49,0.35],[0.32,2.5],[0.27,5.56],[2.34,7.84],[3.35,8.36],[13.67,22.99],[13.72,23.95]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"23","layers":[{"ind":22,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[83.5,25]},"r":{"a":0,"k":0},"s":{"a":0,"k":[167,50]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[20,0],[0,28.5]],"o":[[-4.5,8.17],[-25,0],[0,0]],"v":[[161,20.5],[123.5,44],[83.5,7.5]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[25,0],[4.5,8.17]],"o":[[0,28.5],[-20,0],[0,0]],"v":[[83.5,7.5],[43.5,44],[6,20.5]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[83.5,7.5],[83.5,6]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":12}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"30","layers":[{"ind":29,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[15.5,19]},"r":{"a":0,"k":0},"s":{"a":0,"k":[31,38]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-9,0],[0,0],[-0.07,4.92],[0,0]],"o":[[1,12.67],[0,0],[4.92,0],[0,0],[0,0]],"v":[[0,0],[9,38],[21.62,38],[30.62,29.12],[31,0]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":1}},{"ty":"fl","c":{"a":0,"k":[1,0.91,0.65,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"36","layers":[{"ind":35,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[15.5,19]},"r":{"a":0,"k":0},"s":{"a":0,"k":[31,38]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[9,0],[0,0],[0.07,4.92],[0,0]],"o":[[-1,12.67],[0,0],[-4.92,0],[0,0],[0,0]],"v":[[31,0],[22,38],[9.38,38],[0.38,29.12],[0,0]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":1}},{"ty":"fl","c":{"a":0,"k":[1,0.91,0.65,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"42","layers":[{"ind":41,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[49,25]},"r":{"a":0,"k":0},"s":{"a":0,"k":[98,50]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[5.77,0],[0,0],[1.07,5.67]],"o":[[0,0],[0,0],[0,0],[-1.07,5.67],[0,0],[-5.77,0],[0,0]],"v":[[12.99,33.73],[7.84,6.5],[89.34,6.5],[84.19,33.73],[72.4,43.5],[24.79,43.5],[12.99,33.73]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":13}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[4.33,0],[0,0],[0.8,4.25]],"o":[[0,0],[0,0],[0,0],[-0.8,4.25],[0,0],[-4.32,0],[0,0]],"v":[[9.75,25.3],[5.88,4.88],[67.01,4.88],[63.14,25.3],[54.3,32.63],[18.59,32.63],[9.75,25.3]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"48","layers":[{"ind":47,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[40.5,36.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[81,73]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-18.75,0],[25.5,0],[-0.62,9],[0,21.38]],"o":[[0,21.38],[0.63,9],[-25.5,0],[18.75,0],[0,0]],"v":[[30.05,0],[60.05,27.38],[30.05,54.38],[0.05,27.38],[30.05,0]]}}},{"ty":"fl","c":{"a":0,"k":[0.34,0,0.07,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"65","layers":[{"ind":64,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[80.5,36.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[161,73]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-3.37,6.13],[0,0],[30,9.9],[11.25,-0.37],[0,0],[-18.75,0]],"o":[[15,0],[0,0],[0,-4.12],[0,0],[0,0],[0,21.38],[0,0]],"v":[[90.38,54.43],[118.5,36.81],[120.75,33.43],[86.63,3.06],[60.38,0.06],[60.38,27.06],[90.38,54.43]]}}},{"ty":"fl","c":{"a":0,"k":[0.76,0.56,0.37,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[3.38,6.13],[0,0],[-30,9.9],[-11.25,-0.37],[0,-21.37],[18.75,0]],"o":[[-15,0],[0,0],[0,-4.12],[0,0],[0,0],[0,21.38],[0,0]],"v":[[30.38,54.4],[2.25,36.78],[0,33.4],[34.13,3.03],[60.38,0.03],[60.38,27.03],[30.38,54.4]]}}},{"ty":"fl","c":{"a":0,"k":[0.76,0.56,0.37,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"71","layers":[{"ind":70,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[156.5,134.5]},"r":{"a":0,"k":0},"s":{"a":0,"k":[313,269]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-13.54,-13.54],[13.9,-13.9],[4.9,-2.11],[0,-16.72],[0,0],[-0.1,-0.17],[0,-20.56],[0,0],[24.84,-16.35],[49.62,1.28],[24.92,15.73],[0,25.73],[0,0],[-9.59,15.81],[0,0.38],[0,0.05],[-8.76,13.88],[4.09,4.09],[-13.54,13.54],[0,0],[-13.89,-13.9],[-2.05,-5.84],[-28.73,0],[-21.86,-12.14],[-4.77,4.77]],"o":[[13.89,-13.9],[13.54,13.54],[-4.07,4.07],[8.77,13.89],[0,0],[0,0.38],[9.74,16.06],[0,0],[-0.39,25.15],[-25.03,16.47],[-49.4,-1.27],[-25.22,-15.92],[0,0],[0.19,-19.99],[0.1,-0.17],[0,-0.05],[0,-16.71],[-4.92,-2.11],[-13.9,-13.9],[0,0],[13.54,-13.54],[4.78,4.78],[21.89,-12.17],[28.7,0],[2.06,-5.82],[0,0]],"v":[[244.05,19.54],[293.9,15.49],[289.85,65.34],[276.22,74.64],[290,120.99],[290,121.15],[290.19,121.96],[306,170.99],[305.99,172.19],[268.16,236.82],[156.32,261.48],[44.95,235.01],[7,170.99],[7.01,170.03],[22.81,121.96],[23,121.15],[23,120.99],[36.75,74.68],[23.05,65.34],[19,15.49],[19.01,15.49],[68.85,19.54],[79.16,35.75],[156.5,16.49],[233.74,35.7],[244.05,19.54]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":14}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[-10.16,-10.16],[10.42,-10.42],[3.67,-1.58],[0,-12.54],[0,0],[-0.07,-0.12],[0,-15.42],[0,0],[18.63,-12.26],[37.21,0.96],[18.69,11.8],[0,19.3],[0,0],[-7.19,11.86],[0,0.28],[0,0.04],[-6.57,10.41],[3.07,3.07],[-10.15,10.16],[0,0],[-10.42,-10.42],[-1.54,-4.38],[-21.55,0],[-16.4,-9.11],[-3.58,3.57]],"o":[[10.42,-10.42],[10.15,10.16],[-3.05,3.05],[6.58,10.41],[0,0],[0,0.28],[7.3,12.05],[0,0],[-0.29,18.86],[-18.77,12.35],[-37.05,-0.95],[-18.92,-11.94],[0,0],[0.14,-15],[0.07,-0.12],[0,-0.04],[0,-12.53],[-3.69,-1.58],[-10.42,-10.42],[0,0],[10.15,-10.16],[3.59,3.59],[16.41,-9.13],[21.52,0],[1.54,-4.37],[0,0]],"v":[[183.04,14.65],[220.42,11.62],[217.39,49],[207.17,55.98],[217.5,90.74],[217.5,90.86],[217.64,91.47],[229.5,128.24],[229.49,129.14],[201.12,177.61],[117.24,196.11],[33.71,176.26],[5.25,128.24],[5.25,127.52],[17.11,91.47],[17.25,90.86],[17.25,90.74],[27.56,56.01],[17.29,49],[14.25,11.62],[14.25,11.62],[51.64,14.65],[59.37,26.81],[117.38,12.37],[175.31,26.78],[183.04,14.65]]}}},{"ty":"fl","c":{"a":0,"k":[0.69,0.46,0.28,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"74","layers":[{"ind":11,"ty":0,"parent":7,"ks":{},"w":33,"h":39,"ip":0,"op":97.6,"st":0,"refId":"9"},{"ind":7,"ty":3,"parent":6,"ks":{"s":{"a":0,"k":[197.54,202.52]}},"ip":0,"op":97.6,"st":0},{"ind":6,"ty":3,"parent":5,"ks":{"p":{"a":0,"k":[424.72,-11.847]}},"ip":0,"op":97.6,"st":0},{"ind":17,"ty":0,"parent":13,"ks":{},"w":33,"h":39,"ip":0,"op":97.6,"st":0,"refId":"15"},{"ind":13,"ty":3,"parent":12,"ks":{"s":{"a":0,"k":[203.53,202.52]}},"ip":0,"op":97.6,"st":0},{"ind":12,"ty":3,"parent":5,"ks":{"p":{"a":0,"k":[-11.853,-11.847]}},"ip":0,"op":97.6,"st":0},{"ind":5,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[41.484,30.497]}},"ip":0,"op":97.6,"st":0},{"ind":19,"ty":4,"parent":18,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[38.7,0],[0,30.5],[-38.7,0],[0,-34.6]],"v":[[140.3,43.4],[70.1,98.7],[0,43.4],[70.1,0],[140.3,43.4]],"o":[[0,30.5],[-38.7,0],[0,-34.6],[38.7,0],[0,0]]}}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":18,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[211.372,184.515]}},"ip":0,"op":97.6,"st":0},{"ind":25,"ty":0,"parent":21,"ks":{},"w":167,"h":50,"ip":0,"op":97.6,"st":0,"refId":"23"},{"ind":21,"ty":3,"parent":20,"ks":{"s":{"a":0,"k":[199.91,201.41]}},"ip":0,"op":97.6,"st":0},{"ind":20,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[114.576,269.422]}},"ip":0,"op":97.6,"st":0},{"ind":32,"ty":0,"parent":28,"ks":{},"w":31,"h":38,"ip":0,"op":97.6,"st":0,"refId":"30"},{"ind":28,"ty":3,"parent":27,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":97.6,"st":0},{"ind":27,"ty":3,"parent":26,"ks":{},"ip":0,"op":97.6,"st":0},{"ind":38,"ty":0,"parent":34,"ks":{},"w":31,"h":38,"ip":0,"op":97.6,"st":0,"refId":"36"},{"ind":34,"ty":3,"parent":33,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":97.6,"st":0},{"ind":33,"ty":3,"parent":26,"ks":{"p":{"a":0,"k":[79.017,0]}},"ip":0,"op":97.6,"st":0},{"ind":26,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[211.372,348.405]}},"ip":0,"op":97.6,"st":0},{"ind":44,"ty":0,"parent":40,"ks":{},"w":98,"h":50,"ip":0,"op":97.6,"st":0,"refId":"42"},{"ind":40,"ty":3,"parent":39,"ks":{"s":{"a":0,"k":[197.54,201.41]}},"ip":0,"op":97.6,"st":0},{"ind":39,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[185.691,344.456]}},"ip":0,"op":97.6,"st":0},{"ind":50,"ty":0,"parent":46,"ks":{},"w":81,"h":73,"ip":0,"op":97.6,"st":0,"refId":"48"},{"ind":46,"ty":3,"parent":45,"ks":{"s":{"a":0,"k":[197.54,197.46]}},"ip":0,"op":97.6,"st":0},{"ind":45,"ty":3,"parent":4,"ks":{"a":{"a":0,"k":[80.005,72.072]},"p":{"a":1,"k":[{"t":0,"s":[281.5,357.291],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":1.8,"s":[281.5,357.291],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":49.8,"s":[281.5,363.291],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[281.5,363.291],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":1.8,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":49.8,"s":[122,122],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[122,122],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":54,"ty":4,"parent":53,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[6.914,6.911]},"s":{"a":0,"k":[13.828,13.822]}},{"ty":"fl","c":{"a":0,"k":[1,0.914,0.655]},"o":{"a":0,"k":100}}]},{"ind":53,"ty":3,"parent":52,"ks":{"p":{"a":0,"k":[27.656,21.72]}},"ip":0,"op":97.6,"st":0},{"ind":56,"ty":4,"parent":55,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[34.57,41.466]},"s":{"a":0,"k":[69.14,82.933]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":55,"ty":3,"parent":52,"ks":{},"ip":0,"op":97.6,"st":0},{"ind":52,"ty":3,"parent":51,"ks":{"a":{"a":0,"k":[34.57,41.466]},"p":{"a":0,"k":[277.549,41.466]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":48,"s":[110,110],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[110,110],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":59,"ty":4,"parent":58,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[6.914,6.911]},"s":{"a":0,"k":[13.828,13.822]}},{"ty":"fl","c":{"a":0,"k":[1,0.914,0.655]},"o":{"a":0,"k":100}}]},{"ind":58,"ty":3,"parent":57,"ks":{"p":{"a":0,"k":[27.656,21.72]}},"ip":0,"op":97.6,"st":0},{"ind":61,"ty":4,"parent":60,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[34.57,41.466]},"s":{"a":0,"k":[69.14,82.933]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":60,"ty":3,"parent":57,"ks":{},"ip":0,"op":97.6,"st":0},{"ind":57,"ty":3,"parent":51,"ks":{"a":{"a":0,"k":[34.57,41.466]},"p":{"a":0,"k":[34.57,41.466]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":48,"s":[110,110],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[110,110],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":51,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[124.453,123.303]}},"ip":0,"op":97.6,"st":0},{"ind":67,"ty":0,"parent":63,"ks":{},"w":161,"h":73,"ip":0,"op":97.6,"st":0,"refId":"65"},{"ind":63,"ty":3,"parent":62,"ks":{"s":{"a":0,"k":[201.86,205.48]}},"ip":0,"op":97.6,"st":0},{"ind":62,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[119,210]}},"ip":0,"op":97.6,"st":0},{"ind":73,"ty":0,"parent":69,"ks":{},"w":313,"h":269,"ip":0,"op":97.6,"st":0,"refId":"71"},{"ind":69,"ty":3,"parent":68,"ks":{"s":{"a":0,"k":[197.54,198.19]}},"ip":0,"op":97.6,"st":0},{"ind":68,"ty":3,"parent":4,"ks":{"p":{"a":0,"k":[-27.656,-28.74]}},"ip":0,"op":97.6,"st":0},{"ind":4,"ty":3,"ks":{"p":{"a":0,"k":[28,29]}},"ip":0,"op":97.6,"st":0}]},{"id":"78","layers":[{"ind":76,"ty":0,"ks":{"a":{"a":0,"k":[28,29]},"p":{"a":0,"k":[53,54]}},"w":647,"h":563,"ip":0,"op":97.6,"st":0,"refId":"74"},{"ind":77,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[362.156,320.569]},"r":{"a":0,"k":0},"s":{"a":0,"k":[724.312,641.138]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]}]},{"id":"81","layers":[{"ind":80,"ty":0,"parent":3,"ks":{"p":{"a":0,"k":[-53,-54]}},"ef":[{"ty":29,"ef":[{"ty":0,"nm":"","v":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":45,"s":[0],"i":{"x":0.709,"y":0.415},"o":{"x":0.375,"y":0}},{"t":45.618,"s":[0.01],"i":{"x":0.711,"y":0.506},"o":{"x":0.378,"y":0.124}},{"t":48,"s":[0.15],"i":{"x":0.693,"y":0.363},"o":{"x":0.338,"y":0.092}},{"t":58.5,"s":[4.27],"i":{"x":0.686,"y":0.515},"o":{"x":0.333,"y":0.212}},{"t":63.75,"s":[10.95],"i":{"x":0.673,"y":0.548},"o":{"x":0.313,"y":0.228}},{"t":66.375,"s":[18.04],"i":{"x":0.975,"y":0.746},"o":{"x":0.821,"y":0.341}},{"t":69,"s":[41.67],"h":1},{"t":96.6,"s":[41.67],"h":1}]}},{"ty":7,"nm":"","v":{"a":0,"k":1}},{"ty":7,"nm":"","v":{"a":0,"k":0}}]}],"w":724.3123,"h":641.1378,"ip":0,"op":97.6,"st":0,"refId":"78"},{"ind":3,"ty":3,"ks":{"p":{"a":0,"k":[53,54]}},"ip":0,"op":97.6,"st":0}]},{"id":"92","layers":[{"ind":91,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[52.5,63]},"r":{"a":0,"k":0},"s":{"a":0,"k":[105,126]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-20.71],[0,0],[20.71,0],[0,0],[0,20.71],[0,0],[-20.71,0]],"o":[[20.71,0],[0,0],[0,20.71],[0,0],[-20.71,0],[0,0],[0,-20.71],[0,0]],"v":[[41.25,0],[78.75,37.5],[78.75,57],[41.25,94.5],[37.5,94.5],[0,57],[0,37.5],[37.5,0]]}}},{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-12.22],[0,0],[-12.22,0],[0,12.22],[0,0],[12.22,0]],"o":[[-12.22,0],[0,0],[0,12.22],[12.22,0],[0,0],[0,-12.22],[0,0]],"v":[[39.38,18.75],[17.25,40.88],[17.25,53.63],[39.38,75.75],[61.5,53.63],[61.5,40.88],[39.38,18.75]]}}},{"ty":"fl","c":{"a":0,"k":[1,0.8,0.2,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"97","layers":[{"ind":88,"ty":4,"td":1,"parent":86,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[301,300.951]},"s":{"a":0,"k":[602,601.903]}},{"ty":"fl","c":{"a":0,"k":[0,0,0]},"o":{"a":0,"k":100}}]},{"ind":87,"ty":4,"tt":1,"parent":86,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[301,300.951]},"r":{"a":0,"k":0},"s":{"a":0,"k":[652,651.903]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[301,300.951]},"s":{"a":0,"k":[602,601.903]}},{"ty":"st","c":{"a":0,"k":[0.341,0,0.071]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":50}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":86,"ty":3,"ks":{},"ip":0,"op":97.6,"st":0},{"ind":94,"ty":0,"parent":90,"ks":{},"w":105,"h":126,"ip":0,"op":97.6,"st":0,"refId":"92"},{"ind":90,"ty":3,"parent":89,"ks":{"s":{"a":0,"k":[197.38,197.35]}},"ip":0,"op":97.6,"st":0},{"ind":89,"ty":3,"ks":{"a":{"a":0,"k":[103.623,124.327]},"p":{"a":1,"k":[{"t":0,"s":[301,544.673],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":16.2,"s":[301,544.673],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":64.2,"s":[301,504.673],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[301,504.673],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":16.2,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":64.2,"s":[160,160],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]}},{"t":74.4,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[100,100],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":96,"ty":4,"parent":95,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"el","p":{"a":0,"k":[301,300.951]},"s":{"a":0,"k":[602,601.903]}},{"ty":"fl","c":{"a":0,"k":[0.341,0,0.071]},"o":{"a":0,"k":100}}]},{"ind":95,"ty":3,"ks":{},"ip":0,"op":97.6,"st":0}]},{"id":"101","layers":[{"ind":99,"ty":0,"ks":{"p":{"a":0,"k":[16,16]}},"w":602,"h":704,"ip":0,"op":97.6,"st":0,"refId":"97"},{"ind":100,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[317,367.798]},"r":{"a":0,"k":0},"s":{"a":0,"k":[634,735.596]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]}]},{"id":"104","layers":[{"ind":103,"ty":0,"parent":85,"ks":{"p":{"a":0,"k":[-16,-16]}},"ef":[{"ty":29,"ef":[{"ty":0,"nm":"","v":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":48.6,"s":[0],"i":{"x":0.655,"y":0.185},"o":{"x":0.287,"y":0}},{"t":60.6,"s":[2.94],"i":{"x":0.641,"y":0.471},"o":{"x":0.263,"y":0.171}},{"t":66.6,"s":[8.25],"i":{"x":0.638,"y":0.525},"o":{"x":0.256,"y":0.198}},{"t":69.6,"s":[13.33],"i":{"x":0.993,"y":0.941},"o":{"x":0.949,"y":0.474}},{"t":72.6,"s":[26.67],"h":1},{"t":96.6,"s":[26.67],"h":1}]}},{"ty":7,"nm":"","v":{"a":0,"k":1}},{"ty":7,"nm":"","v":{"a":0,"k":0}}]}],"w":634,"h":735.5965,"ip":0,"op":97.6,"st":0,"refId":"101"},{"ind":85,"ty":3,"ks":{"p":{"a":0,"k":[17,17]}},"ip":0,"op":97.6,"st":0}]},{"id":"111","layers":[{"ind":110,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[191.5,237]},"r":{"a":0,"k":0},"s":{"a":0,"k":[383,474]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]},{"ind":0,"ty":4,"ks":{"s":{"a":0,"k":[133.33,133.33]}},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-101.34],[14.68,-27.74],[20.38,-26.37],[11.11,-11.07],[4.66,4.65],[40.77,52.77],[14.78,27.93],[0,24.43],[-101.34,0]],"o":[[101.34,0],[0,24.43],[-14.78,27.93],[-40.77,52.77],[-4.66,4.65],[-11.11,-11.07],[-20.37,-26.37],[-14.68,-27.74],[0,-101.34],[0,0]],"v":[[191.5,8],[375,191.5],[350.17,271.52],[295.18,354.53],[199.82,462.52],[183.18,462.52],[87.82,354.53],[32.83,271.52],[8,191.5],[191.5,8]]}}},{"ty":"st","c":{"a":0,"k":[0.34,0,0.07,1]},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100},"s":{"a":0,"k":[75,75]}}]},{"ty":"gr","it":[{"ty":"sh","ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,-76.01],[11.01,-20.81],[15.28,-19.78],[8.34,-8.3],[3.49,3.48],[30.57,39.57],[11.08,20.95],[0,18.32],[-76.01,0]],"o":[[76.01,0],[0,18.32],[-11.08,20.95],[-30.57,39.57],[-3.49,3.48],[-8.34,-8.3],[-15.28,-19.78],[-11.01,-20.81],[0,-76.01],[0,0]],"v":[[143.63,6],[281.25,143.63],[262.63,203.64],[221.39,265.89],[149.86,346.89],[137.39,346.89],[65.86,265.89],[24.63,203.64],[6,143.63],[143.63,6]]}}},{"ty":"fl","c":{"a":0,"k":[1,0.8,0.2,1]},"o":{"a":0,"k":100}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"tr","o":{"a":0,"k":100}}]}]}]},{"id":"114","layers":[{"ind":113,"ty":0,"parent":109,"ks":{},"w":383,"h":474,"ip":0,"op":97.6,"st":0,"refId":"111"},{"ind":109,"ty":3,"ks":{"s":{"a":0,"k":[197.39,197.89]}},"ip":0,"op":97.6,"st":0}]},{"id":"118","layers":[{"ind":116,"ty":0,"ks":{"p":{"a":0,"k":[16.667,16.667]}},"w":756,"h":938,"ip":0,"op":97.6,"st":0,"refId":"114"},{"ind":117,"ty":4,"ks":{},"ip":0,"op":97.6,"st":0,"shapes":[{"ty":"rc","p":{"a":0,"k":[394.667,485.667]},"r":{"a":0,"k":0},"s":{"a":0,"k":[789.333,971.333]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}}]}]},{"id":"121","layers":[{"ind":120,"ty":0,"parent":108,"ks":{"p":{"a":0,"k":[-16.667,-16.667]}},"ef":[{"ty":29,"ef":[{"ty":0,"nm":"","v":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":50.4,"s":[0],"i":{"x":0.655,"y":0.185},"o":{"x":0.287,"y":0}},{"t":62.4,"s":[3.06],"i":{"x":0.641,"y":0.471},"o":{"x":0.263,"y":0.171}},{"t":68.4,"s":[8.6],"i":{"x":0.638,"y":0.525},"o":{"x":0.256,"y":0.198}},{"t":71.4,"s":[13.89],"i":{"x":0.993,"y":0.941},"o":{"x":0.949,"y":0.474}},{"t":74.4,"s":[27.78],"h":1},{"t":96.6,"s":[27.78],"h":1}]}},{"ty":7,"nm":"","v":{"a":0,"k":1}},{"ty":7,"nm":"","v":{"a":0,"k":0}}]}],"w":789.3333,"h":971.3333,"ip":0,"op":97.6,"st":0,"refId":"118"},{"ind":108,"ty":3,"ks":{"p":{"a":0,"k":[17,17]}},"ip":0,"op":97.6,"st":0}]}],"fr":60,"h":2622,"ip":0,"layers":[{"ind":83,"ty":0,"parent":2,"ks":{"a":{"a":0,"k":[53,54]},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":45,"s":[100],"i":{"x":1,"y":1},"o":{"x":1,"y":0}},{"t":69,"s":[0],"h":1},{"t":96.6,"s":[0],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":45,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,1],"y":[0,0]}},{"t":69,"s":[0,120.278],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[0,120.278],"h":1}]}},"w":725,"h":642,"ip":0,"op":97.6,"st":0,"refId":"81"},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[281.5,237.5]},"p":{"a":1,"k":[{"t":0,"s":[604.5,1069.5],"i":{"x":[0.022,0.022],"y":[0.959,0.959]},"o":{"x":[0.522,0.522],"y":[0,0]}},{"t":45,"s":[590.519,969.637],"i":{"x":[0.667,0.665],"y":[0.725,0.999]},"o":{"x":[0.334,0.336],"y":[0.392,0.67]}},{"t":45.186,"s":[590.516958215652,969.628],"i":{"x":[0.666,0.666],"y":[0.999,0.331]},"o":{"x":[0.333,0.333],"y":[0.664,0.001]}},{"t":45.618,"s":[590.515,969.6750759742638],"i":{"x":[0.66,0.665],"y":[0.309,0.405]},"o":{"x":[0.326,0.331],"y":[0,0.085]}},{"t":48,"s":[590.577,971.708],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[1,1],"y":[0.08,0.08]}},{"t":69,"s":[604.5,1369.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[604.5,1369.5],"h":1}]},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.022,"y":0.959},"o":{"x":0.522,"y":0}},{"t":45,"s":[-9.99],"h":1},{"t":45.6207,"s":[-9.99],"i":{"x":0.66,"y":0.309},"o":{"x":0.326,"y":0}},{"t":48,"s":[-9.94],"i":{"x":1,"y":1},"o":{"x":1,"y":0.08}},{"t":69,"s":[0],"h":1},{"t":96.6,"s":[0],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":0.6,"s":[100,100],"i":{"x":[0.023,0.023],"y":[0.958,0.958]},"o":{"x":[0.522,0.522],"y":[0,0]}},{"t":45,"s":[119.97,119.97],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[1,1],"y":[0,0]}},{"t":69,"s":[80,80],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[80,80],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":106,"ty":0,"parent":84,"ks":{"a":{"a":0,"k":[17,17]},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":48.6,"s":[100],"i":{"x":1,"y":1},"o":{"x":1,"y":0}},{"t":72.6,"s":[0],"h":1},{"t":96.6,"s":[0],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":48.6,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,1],"y":[0,0]}},{"t":72.6,"s":[0,152.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[0,152.5],"h":1}]}},"w":635,"h":737,"ip":0,"op":97.6,"st":0,"refId":"104"},{"ind":84,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[301,334.5]},"p":{"a":1,"k":[{"t":0,"s":[603,1258.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":48,"s":[603,1238.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[603,1238.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":48,"s":[125,125],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[125,125],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":123,"ty":0,"parent":107,"ks":{"a":{"a":0,"k":[17,17]},"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":50.4,"s":[100],"i":{"x":1,"y":1},"o":{"x":1,"y":0}},{"t":74.4,"s":[0],"h":1},{"t":96.6,"s":[0],"h":1}]},"p":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":50.4,"s":[0,0],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,1],"y":[0,0]}},{"t":74.4,"s":[0,191.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[0,191.5],"h":1}]}},"w":790,"h":972,"ip":0,"op":97.6,"st":0,"refId":"121"},{"ind":107,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[378,469]},"p":{"a":0,"k":[603,1322]},"s":{"a":1,"k":[{"t":0,"s":[100,100],"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.5,0.5],"y":[0,0]}},{"t":48,"s":[120,120],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":96.6,"s":[120,120],"h":1}]}},"ip":0,"op":97.6,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":97.6,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":97.6,"st":0}],"meta":{"g":"https://jitter.video"},"op":96.6,"v":"5.7.4","w":1206} \ No newline at end of file diff --git a/assets/loader.json b/assets/lottie/loader.json similarity index 100% rename from assets/loader.json rename to assets/lottie/loader.json diff --git a/assets/siedwalkChip.svg b/assets/siedwalkChip.svg deleted file mode 100644 index 3b323f8..0000000 --- a/assets/siedwalkChip.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/skywayChip.svg b/assets/skywayChip.svg deleted file mode 100644 index a08971e..0000000 --- a/assets/skywayChip.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/splashDark.png b/assets/splashDark.png deleted file mode 100644 index b9cfae8..0000000 Binary files a/assets/splashDark.png and /dev/null differ diff --git a/assets/splashIcon.svg b/assets/splashIcon.svg deleted file mode 100644 index e16640e..0000000 --- a/assets/splashIcon.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/splashLight.png b/assets/splashLight.png deleted file mode 100644 index d29a8a1..0000000 Binary files a/assets/splashLight.png and /dev/null differ diff --git a/assets/tunnelChip.svg b/assets/tunnelChip.svg deleted file mode 100644 index ff8fa8f..0000000 --- a/assets/tunnelChip.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/bun.lock b/bun.lock index 83fda7e..c42d059 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@expo/app-integrity": "^0.1.9", "@expo/vector-icons": "^15.0.2", "@noble/hashes": "^2.0.1", + "@react-native-community/netinfo": "^11.4.1", "@react-native-masked-view/masked-view": "^0.3.1", "@rnmapbox/maps": "^10.2.6", "axios": "^1.9.0", @@ -25,7 +26,7 @@ "expo-sensors": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", - "lottie-react-native": "~7.3.1", + "lottie-react-native": "^7.3.4", "moti": "^0.30.0", "prop-types": "^15.8.1", "react": "19.1.0", @@ -39,6 +40,7 @@ "react-native-svg": "15.12.1", "react-native-svg-transformer": "^1.3.0", "react-native-worklets": "0.5.1", + "sonner-native": "^0.21.2", "styled-components": "^6.1.8", }, "devDependencies": { @@ -438,6 +440,8 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@react-native-community/netinfo": ["@react-native-community/netinfo@11.4.1", "", { "peerDependencies": { "react-native": ">=0.59" } }, "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg=="], + "@react-native-masked-view/masked-view": ["@react-native-masked-view/masked-view@0.3.2", "", { "peerDependencies": { "react": ">=16", "react-native": ">=0.57" } }, "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ=="], "@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="], @@ -1670,6 +1674,8 @@ "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + "sonner-native": ["sonner-native@0.21.2", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.10.1", "react-native-safe-area-context": ">=4.10.5", "react-native-screens": ">=3.31.1", "react-native-svg": ">=15.6.0" } }, "sha512-LnGPmfgzrNIwcc+FvcLJqx8aH1dEHePRzvNR8aIR4kl9spySRkXK160GmQIazjfm6mSMlPqZwRa5eycvrzg/eQ=="], + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], diff --git a/components/Banner/BannerButton/index.tsx b/components/Banner/BannerButton/index.tsx new file mode 100644 index 0000000..2a59e8f --- /dev/null +++ b/components/Banner/BannerButton/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { MotiPressableProps } from "moti/interactions"; +import { defaultMotiPressableProps, StyledText } from "../../../styles/global"; +import { Container } from "./styles"; + +interface BannerButtonProps extends MotiPressableProps { + label: string; +} + +const BannerButton: React.FC = ({ label, ...props }) => { + return ( + + + {label} + + + ); +}; + +export default BannerButton; diff --git a/components/Banner/BannerButton/styles.ts b/components/Banner/BannerButton/styles.ts new file mode 100644 index 0000000..7df5e72 --- /dev/null +++ b/components/Banner/BannerButton/styles.ts @@ -0,0 +1,9 @@ +import { MotiPressable } from "moti/interactions"; +import styled from "styled-components/native"; + +export const Container = styled(MotiPressable)` + background-color: #00000020; + padding: 8px; + padding-top: 6px; + border-radius: 6px; +`; diff --git a/components/Banner/index.tsx b/components/Banner/index.tsx new file mode 100644 index 0000000..8607bac --- /dev/null +++ b/components/Banner/index.tsx @@ -0,0 +1,192 @@ +import React, { useEffect, useRef, useState } from "react"; +import { MaterialIcons } from "@expo/vector-icons"; +import { Container, SideContainer, TextContainer } from "./styles"; +import { StyledText } from "../../styles/global"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import BannerButton from "./BannerButton"; +import Svg, { Circle, Text as SvgText } from "react-native-svg"; +import Animated, { + FadeInRight, + FadeOutRight, + SlideInUp, + SlideOutUp, + useAnimatedStyle, + useSharedValue, + withSpring, +} from "react-native-reanimated"; +import { MotiView } from "moti"; + +interface BannerProps { + title: string; + description: string; + noButton?: boolean; + retryCooldown?: number; + onPressButton?: () => void; + retries?: number; +} + +const foregroundColor = "#fff"; + +const Banner: React.FC = ({ + title, + description, + retryCooldown, + onPressButton, + retries, + noButton, +}) => { + const insets = useSafeAreaInsets(); + const totalTime = retryCooldown || 10; + const radius = 11; + const circumference = 2 * Math.PI * radius; + const [timeLeft, setTimeLeft] = useState(totalTime); + const [resetToken, setResetToken] = useState(0); + const [started, setStarted] = useState(false); + const shakeX = useSharedValue(0); + const hasMountedRetries = useRef(false); + const previousRetries = useRef(retries); + + useEffect(() => { + if (!started) return; + + const startTime = Date.now(); + setTimeLeft(totalTime); + const intervalId = setInterval(() => { + const elapsedSeconds = (Date.now() - startTime) / 1000; + const remaining = Math.max(totalTime - elapsedSeconds, 0); + setTimeLeft(remaining); + + if (remaining === 0) { + clearInterval(intervalId); + } + }, 100); + + return () => clearInterval(intervalId); + }, [resetToken, totalTime, started]); + + const progress = timeLeft / totalTime; + const strokeDashoffset = circumference * (1 - progress); + const displayValue = Math.max(0, Math.ceil(timeLeft)); + const isRunning = started && displayValue > 0; + + const shakeStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: shakeX.value }], + })); + + useEffect(() => { + if (hasMountedRetries.current && previousRetries.current !== retries) { + shakeX.value = 10; + shakeX.value = withSpring(0, { + damping: 3, + stiffness: 250, + mass: 0.5, + }); + } + + hasMountedRetries.current = true; + previousRetries.current = retries; + }, [retries, shakeX]); + + const restartTimer = () => { + setStarted(true); + setResetToken((prev) => prev + 1); + setTimeLeft(totalTime); + }; + + return ( + + + + + + + + {title} + + + {description} + + + {!noButton && ( + + {isRunning && ( + + + + {displayValue} + + + + + )} + + { + onPressButton(); + restartTimer(); + } + : restartTimer + } + /> + + + )} + + + ); +}; + +export default Banner; diff --git a/components/Banner/styles.ts b/components/Banner/styles.ts new file mode 100644 index 0000000..7732929 --- /dev/null +++ b/components/Banner/styles.ts @@ -0,0 +1,26 @@ +import Animated from "react-native-reanimated"; +import styled from "styled-components/native"; + +export const Container = styled(Animated.View)` + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: 25px 20px; + background-color: ${({ theme }) => theme.colors.error}f2; + flex-direction: row; + gap: 15px; + align-items: center; + z-index: 999999999; + overflow: hidden; +`; + +export const TextContainer = styled.View` + flex-direction: column; + flex: 1; +`; + +export const SideContainer = styled.View` + position: relative; + flex-direction: row; +`; diff --git a/components/CustomMarker/index.tsx b/components/CustomMarker/index.tsx index 0541594..35e10a1 100644 --- a/components/CustomMarker/index.tsx +++ b/components/CustomMarker/index.tsx @@ -1,14 +1,18 @@ -import React, { ComponentProps } from "react"; -import { View } from "react-native"; -import CustomMark from "../../assets/customMarker.svg"; +import React from "react"; +import { Pressable } from "react-native"; import MapboxGL from "@rnmapbox/maps"; +import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated"; +import CustomMark from "../../assets/images/customMarker.svg"; +import { MotiView } from "moti"; -export interface Props - extends Omit, "children"> { +export interface Props { + id: string; width?: number; height?: number; coordinate: [number, number]; - popupText?: string; + delay?: number; + onSelected?: (id: string) => void; + selected?: boolean; } /** @@ -37,19 +41,46 @@ export interface Props * ``` */ -const CustomMarker: React.FC = (props: Props) => { - const { width = 50, height = 50, coordinate, ...otherProps } = props; +const CustomMarker: React.FC = ({ + id, + width = 50, + height = 50, + coordinate, + delay = 80, + onSelected, + selected, +}) => { + const handlePress = React.useCallback(() => { + onSelected?.(id); + }, [id, onSelected, selected]); return ( - - - - - + + + + + + + + ); }; diff --git a/components/DirectionsModal/index.tsx b/components/DirectionsModal/index.tsx index d8c774f..820b957 100644 --- a/components/DirectionsModal/index.tsx +++ b/components/DirectionsModal/index.tsx @@ -270,15 +270,15 @@ const DirectionsModal: React.FC = ({ transform: [{ translateY: legendTranslation.value }], })); - const pointerEvents = active ? "auto" : "none"; + const pointerEvents = active ? "box-none" : "none"; return ( - + - + Legend {legendChipsData.map((props) => ( diff --git a/components/Loader/index.tsx b/components/Loader/index.tsx index e146e13..f02c0e2 100644 --- a/components/Loader/index.tsx +++ b/components/Loader/index.tsx @@ -4,7 +4,7 @@ import LottieView from "lottie-react-native"; export default function Animation() { return ( : -// +import { interpolateCoords } from "../../utils/functions"; +import type { PathComponentProps } from "./types"; const FEATURE_COLLECTION_EMPTY = { type: "FeatureCollection", features: [], -} as const; - -/** - * @description Component that animates a path on a MapboxGL map using an easing-based "draw-on" animation. - * - * @param {PathComponentProps['coordinates']} coordinates - Array of [longitude, latitude] coordinate pairs that define the full path to be drawn. - * @param {PathComponentProps['lineColor']} [lineColor] - Optional color of the line. Defaults to theme `tunnel2` color if not provided. - * @param {PathComponentProps['lineWidth']} [lineWidth=6] - Optional width of the line in pixels. - * - * @returns {React.FC} TSX React Functional Component - * - * @example - * ```tsx - * - * ``` - */ +}; +const AnimatedShapeSource = Animated.createAnimatedComponent( + MapboxGL.ShapeSource, + { jsProps: ["shape"] }, +); + +type StartMeta = { segmentIndex: number; ratio: number }; + +type SharedRoute = { + groups: NormalizedGroup[]; + totalLength: number; +}; + +const clampWorklet = (value: number, min: number, max: number) => { + "worklet"; + return Math.min(Math.max(value, min), max); +}; + +const pointsEqualWorklet = (a?: [number, number], b?: [number, number]) => { + "worklet"; + if (!a || !b) return false; + return ( + Math.abs(a[0] - b[0]) < COORD_EPS && + Math.abs(a[1] - b[1]) < COORD_EPS + ); +}; + +const locateOnGroupWorklet = ( + group: NormalizedGroup, + distance: number, +): { index: number; ratio: number } => { + "worklet"; + if (!group.segmentLengths.length) + return { index: 0, ratio: distance > 0 ? 1 : 0 }; + if (distance <= 0) return { index: 0, ratio: 0 }; + let remaining = distance; + for (let i = 0; i < group.segmentLengths.length; i++) { + const len = group.segmentLengths[i]; + if (remaining <= len) { + const ratio = len === 0 ? 1 : remaining / len; + return { index: i, ratio }; + } + remaining -= len; + } + return { index: group.segmentLengths.length - 1, ratio: 1 }; +}; + +const pointOnGroupWorklet = ( + group: NormalizedGroup, + position: { index: number; ratio: number }, +): [number, number] => { + "worklet"; + const { index, ratio } = position; + const start = group.coords[index]; + const end = group.coords[index + 1] || start; + if (!end) return start; + if (ratio <= 0) return start; + if (ratio >= 1) return end; + return interpolateCoords(start, end, ratio) as [number, number]; +}; + +const buildPartialFeatureWorklet = ( + group: NormalizedGroup, + startWithin: number, + endWithin: number, + options?: { + startOverride?: { index: number; ratio: number }; + }, +) => { + "worklet"; + if (endWithin - startWithin <= LENGTH_EPS) return null; + const startPos = + options?.startOverride ?? locateOnGroupWorklet(group, startWithin); + const endPos = locateOnGroupWorklet(group, endWithin); + + const coords: [number, number][] = []; + + const push = (point: [number, number]) => { + const last = coords[coords.length - 1]; + if (!pointsEqualWorklet(last, point)) coords.push(point); + }; + + push(pointOnGroupWorklet(group, startPos)); + + if (startPos.index === endPos.index) { + push(pointOnGroupWorklet(group, endPos)); + } else { + if (startPos.ratio < 1 - COORD_EPS) { + push(group.coords[startPos.index + 1]); + } + + for (let i = startPos.index + 1; i < endPos.index; i++) { + push(group.coords[i + 1]); + } + + push(pointOnGroupWorklet(group, endPos)); + } + + if (coords.length < 2) return null; + + return { + type: "Feature", + properties: { color: group.color }, + geometry: { type: "LineString", coordinates: coords }, + }; +}; + +const buildSliceWorklet = ( + groups: NormalizedGroup[], + totalLength: number, + startDistance: number, + endDistance: number, + startMeta?: StartMeta | null, +) => { + "worklet"; + if (!groups.length) return []; + + const clampedStart = clampWorklet(startDistance, 0, totalLength); + const clampedEnd = clampWorklet(endDistance, clampedStart, totalLength); + if (clampedEnd - clampedStart <= LENGTH_EPS) return []; + + const features: any[] = []; + let travelled = 0; + + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const groupStart = travelled; + const groupEnd = groupStart + group.totalLength; + const groupFirstSegment = group.startSegmentIndex; + const groupLastSegment = + group.startSegmentIndex + group.segmentLengths.length - 1; + const startWithinGroup = + !!startMeta && + startMeta.segmentIndex >= groupFirstSegment && + startMeta.segmentIndex <= groupLastSegment; + + if (!startWithinGroup && groupEnd < clampedStart - LENGTH_EPS) { + travelled = groupEnd; + continue; + } + if (groupStart > clampedEnd + LENGTH_EPS) break; + + let localStart = Math.max(0, clampedStart - groupStart); + const localEnd = Math.min(group.totalLength, clampedEnd - groupStart); + + let startOverride: { index: number; ratio: number } | undefined; + + if (startWithinGroup && startMeta) { + const segmentIdx = startMeta.segmentIndex - groupFirstSegment; + const rawRatio = startMeta.ratio; + const safeRatio = + rawRatio !== rawRatio ? 0 : Math.min(1, Math.max(0, rawRatio)); + let before = 0; + for (let j = 0; j < segmentIdx; j++) { + before += group.segmentLengths[j] ?? 0; + } + const segLen = group.segmentLengths[segmentIdx] ?? 0; + localStart = before + segLen * safeRatio; + startOverride = { index: segmentIdx, ratio: safeRatio }; + } + + if ( + localStart <= LENGTH_EPS && + group.totalLength - localEnd <= LENGTH_EPS + ) { + features.push({ + type: "Feature", + properties: { color: group.color }, + geometry: { type: "LineString", coordinates: group.coords }, + }); + } else { + const partial = buildPartialFeatureWorklet(group, localStart, localEnd, { + startOverride, + }); + if (partial) features.push(partial); + } + + travelled = groupEnd; + if (clampedEnd <= groupEnd + LENGTH_EPS) break; + } + + return features; +}; + +// Example usage, place in : +// const PathComponent: React.FC = ({ id, coordinates, @@ -59,33 +219,27 @@ const PathComponent: React.FC = ({ userLocation, }) => { const theme = useTheme(); - const [featureCollection, setFeatureCollection] = useState( - FEATURE_COLLECTION_EMPTY, - ); - const animationFrameRef = useRef(null); - const prevTotalLengthRef = useRef(null); - const prevStartOffsetRef = useRef(null); + const { colors } = theme; + const singleColor = lineColor || colors.tunnel2; - const singleColor = lineColor || theme.colors.tunnel2; - // Normalize to grouped form and precompute metadata for animation const normalized = useMemo(() => { - // Helper: color mapping by type const mapTypeToColor = (t?: string): string => { const key = (t || "").toLowerCase(); - if (key.includes("tunnel")) return theme.colors.tunnel2; - if (key.includes("skyway")) return theme.colors.skyway2; + if (key.includes("tunnel")) return colors.tunnel2; + if (key.includes("skyway")) return colors.skyway2; if (key.includes("sidewalk") || key.includes("outdoor")) - return theme.colors.sidewalk2; - if (key.includes("mapbox")) return theme.colors.sidewalk2; - return singleColor; // fallback + return colors.sidewalk2; + if (key.includes("mapbox")) return colors.sidewalk2; + return singleColor; }; const groups: NormalizedGroup[] = []; let segmentCursor = 0; + if (segments && segments.length) { for (const seg of segments) { const coords = (seg.coordinates || []).filter(Array.isArray); - if (!coords || coords.length < 2) continue; // need at least a segment + if (!coords || coords.length < 2) continue; const color = seg.color || mapTypeToColor(seg.type); const typedCoords = coords as [number, number][]; const lengths: number[] = []; @@ -131,7 +285,7 @@ const PathComponent: React.FC = ({ groups, totalLength: groups.reduce((sum, g) => sum + g.totalLength, 0), }; - }, [segments, coordinates, singleColor, theme.colors]); + }, [segments, coordinates, singleColor, colors]); const routePolyline = useMemo(() => { const coords: [number, number][] = []; @@ -160,205 +314,135 @@ const PathComponent: React.FC = ({ return projectOntoPolyline(userPoint, routePolyline); }, [routePolyline, userPoint]); - const startMeta = useMemo( + const startMeta = useMemo( () => userProjection ? { segmentIndex: userProjection.segmentIndex, ratio: userProjection.segmentT, } - : undefined, + : null, [userProjection], ); const projectedDistance = userProjection?.distanceAlong ?? 0; + const clampedStart = useMemo( + () => Math.max(0, Math.min(projectedDistance, normalized.totalLength)), + [projectedDistance, normalized.totalLength], + ); - const fullFeatures = useMemo(() => { - return normalized.groups.map((g) => ({ - type: "Feature", - properties: { color: g.color }, - geometry: { type: "LineString", coordinates: g.coords }, - })); - }, [normalized.groups]); - - const buildSlice = useCallback( - ( - startDistance: number, - endDistance: number, - startMeta?: { segmentIndex: number; ratio: number | null }, - ) => { - if (!normalized.groups.length) return []; - const clampedStart = Math.max( - 0, - Math.min(startDistance, normalized.totalLength), - ); - const clampedEnd = Math.max( - clampedStart, - Math.min(endDistance, normalized.totalLength), - ); - if (clampedEnd - clampedStart <= LENGTH_EPS) return []; - - const features: any[] = []; - let travelled = 0; - - for (let i = 0; i < normalized.groups.length; i++) { - const group = normalized.groups[i]; - const groupStart = travelled; - const groupEnd = groupStart + group.totalLength; - const groupFirstSegment = group.startSegmentIndex; - const groupLastSegment = - group.startSegmentIndex + group.segmentLengths.length - 1; - const startWithinGroup = - startMeta && - startMeta.segmentIndex >= groupFirstSegment && - startMeta.segmentIndex <= groupLastSegment; - - if (!startWithinGroup && groupEnd < clampedStart - LENGTH_EPS) { - travelled = groupEnd; - continue; - } - if (groupStart > clampedEnd + LENGTH_EPS) break; - - let localStart = Math.max(0, clampedStart - groupStart); - const localEnd = Math.min(group.totalLength, clampedEnd - groupStart); - - let startOverride: - | { - index: number; - ratio: number; - } - | undefined; - - if (startWithinGroup) { - const segmentIdx = startMeta!.segmentIndex - groupFirstSegment; - const safeRatio = - startMeta!.ratio === null || Number.isNaN(startMeta!.ratio) - ? 0 - : Math.min(1, Math.max(0, startMeta!.ratio)); - const before = group.segmentLengths - .slice(0, segmentIdx) - .reduce((sum, len) => sum + len, 0); - const segLen = group.segmentLengths[segmentIdx] ?? 0; - localStart = before + segLen * safeRatio; - startOverride = { index: segmentIdx, ratio: safeRatio }; - } - - if ( - localStart <= LENGTH_EPS && - group.totalLength - localEnd <= LENGTH_EPS - ) { - features.push(fullFeatures[i]); - } else { - const partial = buildPartialFeature(group, localStart, localEnd, { - startOverride, - }); - if (partial) features.push(partial); - } + const routeData = useSharedValue({ + groups: [], + totalLength: 0, + }); + const startDistance = useSharedValue(0); + const startMetaShared = useSharedValue(null); + const progress = useSharedValue(0); - travelled = groupEnd; - if (clampedEnd <= groupEnd + LENGTH_EPS) break; - } - return features; - }, - [normalized.groups, normalized.totalLength, fullFeatures], - ); + const prevTotalLengthRef = useRef(null); + const prevStartOffsetRef = useRef(null); useEffect(() => { - if (animationFrameRef.current !== null) { - cancelAnimationFrame(animationFrameRef.current); - animationFrameRef.current = null; - } + routeData.value = { + groups: normalized.groups, + totalLength: normalized.totalLength, + }; + startDistance.value = clampedStart; + startMetaShared.value = startMeta; + }, [ + normalized.groups, + normalized.totalLength, + clampedStart, + startMeta, + routeData, + startDistance, + startMetaShared, + ]); + useEffect(() => { const routeLength = normalized.totalLength; - const clampedStart = Math.max(0, Math.min(projectedDistance, routeLength)); const remainingLength = Math.max(0, routeLength - clampedStart); const wasLength = prevTotalLengthRef.current; + const prevStart = prevStartOffsetRef.current; const isNewRoute = wasLength === null || Math.abs(wasLength - routeLength) > LENGTH_EPS; - const prevStart = prevStartOffsetRef.current; const startReset = prevStart !== null && Math.abs(prevStart - clampedStart) > LENGTH_EPS && clampedStart < prevStart; + prevTotalLengthRef.current = routeLength; prevStartOffsetRef.current = clampedStart; if (!normalized.groups.length || remainingLength <= LENGTH_EPS) { - setFeatureCollection(FEATURE_COLLECTION_EMPTY); + cancelAnimation(progress); + progress.value = 0; return; } if (!isNewRoute && !startReset) { - const features = buildSlice(clampedStart, routeLength, startMeta); - setFeatureCollection( - features.length - ? { - type: "FeatureCollection", - features, - } - : FEATURE_COLLECTION_EMPTY, - ); + cancelAnimation(progress); + progress.value = 1; return; } const durationAuto = (() => { - const base = remainingLength * 6; // ~6ms per meter + const base = remainingLength * 6; return Math.min(4000, Math.max(900, base)); })(); const duration = durationMs ?? durationAuto; - const start = - typeof performance !== "undefined" ? performance.now() : Date.now(); - - const render = (fraction: number) => { - const eased = Math.max(0, Math.min(1, fraction)); - const endDistance = clampedStart + remainingLength * eased; - const features = buildSlice(clampedStart, endDistance, startMeta); - setFeatureCollection( - features.length - ? { - type: "FeatureCollection", - features, - } - : FEATURE_COLLECTION_EMPTY, - ); - }; - render(0); + cancelAnimation(progress); + progress.value = 0; + progress.value = withTiming(1, { + duration, + easing: Easing.inOut(Easing.cubic), + }); + }, [ + normalized.groups.length, + normalized.totalLength, + clampedStart, + durationMs, + progress, + ]); - const step = (now: number) => { - const elapsed = now - start; - const raw = duration === 0 ? 1 : Math.min(1, elapsed / duration); - render(easeInOutCubic(raw)); - if (raw < 1) { - animationFrameRef.current = requestAnimationFrame(step); - } else { - animationFrameRef.current = null; - } - }; + const animatedProps = useAnimatedProps(() => { + const data = routeData.value; + if (!data.groups.length || data.totalLength <= LENGTH_EPS) { + return { shape: FEATURE_COLLECTION_EMPTY }; + } - animationFrameRef.current = requestAnimationFrame(step); + const clamped = clampWorklet(startDistance.value, 0, data.totalLength); + const remaining = Math.max(0, data.totalLength - clamped); + const endDistance = clamped + remaining * progress.value; - return () => { - if (animationFrameRef.current !== null) { - cancelAnimationFrame(animationFrameRef.current); - animationFrameRef.current = null; - } + const features = buildSliceWorklet( + data.groups, + data.totalLength, + clamped, + endDistance, + startMetaShared.value, + ); + + if (features.length === 0) { + return { shape: FEATURE_COLLECTION_EMPTY }; + } + + return { + shape: { + type: "FeatureCollection", + features, + }, }; - }, [ - buildSlice, - durationMs, - normalized.groups, - normalized.totalLength, - projectedDistance, - startMeta, - ]); + }); return ( - = ({ lineEmissiveStrength: 1.0, }} /> - + ); }; diff --git a/components/Searchbar/SearchResult/index.tsx b/components/Searchbar/SearchResult/index.tsx index 1dfdbd4..b1bfb39 100644 --- a/components/Searchbar/SearchResult/index.tsx +++ b/components/Searchbar/SearchResult/index.tsx @@ -6,7 +6,7 @@ import { BuildingName, BuildingAddress, } from "./styles"; -import house from "../../../assets/m-house.png"; +import house from "../../../assets/images/m-house.png"; import { GetSearchResponse } from "../../../@types/api"; interface SearchResultProps extends ComponentProps { diff --git a/components/Searchbar/index.tsx b/components/Searchbar/index.tsx index 2556fd5..4247287 100644 --- a/components/Searchbar/index.tsx +++ b/components/Searchbar/index.tsx @@ -1,4 +1,4 @@ -import React, { ComponentProps, useEffect, useRef, useState } from "react"; +import React, { ComponentProps, useRef, useState } from "react"; import { ScrollView, TextInput } from "react-native"; import { Bar, Container, SearchInput, SearchResultContainer } from "./styles"; import { useTheme } from "styled-components/native"; @@ -10,7 +10,8 @@ import Animated, { SlideInUp, SlideOutUp, } from "react-native-reanimated"; -import { getPopular, getSearchResults } from "../../services/api"; +import { toast } from "sonner-native"; +import { getSearchResults } from "../../services/api"; import { GetPopularResponse, GetSearchResponse } from "../../@types/api"; import { devLog } from "../../utils/functions"; @@ -22,8 +23,13 @@ interface SearchbarProps extends ComponentProps { onSelectDestination: ( dest: BuildingInfo | GetPopularResponse[number], ) => void; + popularDestinations?: GetPopularResponse; } +const TOAST_IDS = { + search: "search-error", +} as const; + /** * @description A styled search bar with text input * @@ -44,29 +50,17 @@ interface SearchbarProps extends ComponentProps { const Searchbar: React.FC = ({ onSelectDestination, + popularDestinations = [], ...props }) => { const theme = useTheme(); const [inputText, setInputText] = React.useState(""); const [buildingResults, setBuildingResults] = useState([]); - const [popularDestinations, setPopularDestinations] = - useState([]); const inputRef = useRef(null); const showResults = inputRef.current?.isFocused() && buildingResults.length > 0 && inputText; - useEffect(() => { - (async () => { - try { - devLog("popular destinations: ", await getPopular()); - setPopularDestinations(await getPopular()); - } catch (e) { - devLog("error getting popular routes: ", e); - } - })(); - }, []); - return ( = ({ value={inputText} onChangeText={async (newVal) => { setInputText(newVal); - // TODO: modify search logic - if (newVal) - setBuildingResults(await getSearchResults(newVal.toLowerCase())); + const trimmed = newVal.trim(); + if (!trimmed) { + setBuildingResults([]); + return; + } + try { + const results = await getSearchResults(trimmed.toLowerCase()); + setBuildingResults(results); + toast.dismiss(TOAST_IDS.search); + } catch (e) { + devLog("error getting search results: ", e); + toast.error("Search failed", { + id: TOAST_IDS.search, + description: "Check your connection and try again.", + }); + } }} {...props} ref={inputRef} diff --git a/components/SplashIcon/index.tsx b/components/SplashIcon/index.tsx index ed5aa71..55b152f 100644 --- a/components/SplashIcon/index.tsx +++ b/components/SplashIcon/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Dimensions, View } from 'react-native'; -import Icon from '../../assets/splashIcon.svg'; +import Icon from '../../assets/images/splashIcon.svg'; const {width, height} = Dimensions.get('window'); diff --git a/components/SplashScreen/index.tsx b/components/SplashScreen/index.tsx new file mode 100644 index 0000000..f14b6ca --- /dev/null +++ b/components/SplashScreen/index.tsx @@ -0,0 +1,30 @@ +import React, { ComponentProps, forwardRef } from "react"; +import { Container } from "./styles"; +import LottieView, { LottieViewProps } from "lottie-react-native"; +import LogoAnimationIn from "../../assets/lottie/animated-logo-in.json"; +import LogoAnimationOut from "../../assets/lottie/animated-logo-out.json"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface SplashScreenProps extends Omit { + animatedContainerProps?: ComponentProps; + variant?: "in" | "out"; +} + +// eslint-disable-next-line react/display-name +const SplashScreen = forwardRef( + ({ variant = "out", animatedContainerProps, ...props }, ref) => { + return ( + + + + ); + }, +); + +export default SplashScreen; diff --git a/components/SplashScreen/styles.ts b/components/SplashScreen/styles.ts new file mode 100644 index 0000000..68c686e --- /dev/null +++ b/components/SplashScreen/styles.ts @@ -0,0 +1,13 @@ +import Animated from "react-native-reanimated"; +import styled from "styled-components/native"; + +export const Container: Animated.View = styled(Animated.View)` + position: absolute; + z-index: 99999; + flex: 1; + height: 100%; + width: 100%; + place-content: center; + place-items: center; + background-color: ${({ theme }) => theme.colors.splash}; +`; diff --git a/components/ToastHost.tsx b/components/ToastHost.tsx new file mode 100644 index 0000000..a5e189c --- /dev/null +++ b/components/ToastHost.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { useColorScheme } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Toaster } from "sonner-native"; +import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; +import { useTheme } from "styled-components/native"; + +const ToastHost = () => { + const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme(); + const theme = useTheme(); + + return ( + + ), + error: ( + + ), + info: ( + + ), + }} + /> + ); +}; + +export default ToastHost; diff --git a/package.json b/package.json index 7cb9ae6..d704d70 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@expo/app-integrity": "^0.1.9", "@expo/vector-icons": "^15.0.2", "@noble/hashes": "^2.0.1", + "@react-native-community/netinfo": "^11.4.1", "@react-native-masked-view/masked-view": "^0.3.1", "@rnmapbox/maps": "^10.2.6", "axios": "^1.9.0", @@ -31,7 +32,7 @@ "expo-sensors": "~15.0.7", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", - "lottie-react-native": "~7.3.1", + "lottie-react-native": "^7.3.4", "moti": "^0.30.0", "prop-types": "^15.8.1", "react": "19.1.0", @@ -45,6 +46,7 @@ "react-native-svg": "15.12.1", "react-native-svg-transformer": "^1.3.0", "react-native-worklets": "0.5.1", + "sonner-native": "^0.21.2", "styled-components": "^6.1.8" }, "devDependencies": { diff --git a/services/api.ts b/services/api.ts index 91b1440..29d4800 100644 --- a/services/api.ts +++ b/services/api.ts @@ -91,19 +91,28 @@ const normalizePathWithQuery = (pathWithQuery: string): string => { if (!queryString) return pathname; + const safeDecode = (value: string) => { + try { + return decodeURIComponent(value); + } catch { + return value; + } + }; + + const normalizeQueryComponent = (value: string) => + encodeURIComponent(safeDecode(value.replace(/\+/g, " "))); + const normalizedParts = queryString .split("&") .filter(Boolean) .map((part) => { const equalsIndex = part.indexOf("="); if (equalsIndex === -1) { - return encodeURIComponent(part.replace(/\+/g, " ")); + return normalizeQueryComponent(part); } const key = part.slice(0, equalsIndex); const value = part.slice(equalsIndex + 1); - return `${encodeURIComponent(key.replace(/\+/g, " "))}=${encodeURIComponent( - value.replace(/\+/g, " "), - )}`; + return `${normalizeQueryComponent(key)}=${normalizeQueryComponent(value)}`; }); return normalizedParts.length diff --git a/styles/themes/dark.ts b/styles/themes/dark.ts index 9e26127..30ca18e 100644 --- a/styles/themes/dark.ts +++ b/styles/themes/dark.ts @@ -7,6 +7,7 @@ const darkTheme = { medium: 24, }, colors: { + splash: "#37000B", secondary1: "#3c000b", secondary2: "#4c000f", secondary3: "#5c0013", diff --git a/styles/themes/light.ts b/styles/themes/light.ts index 3d3e332..353207b 100644 --- a/styles/themes/light.ts +++ b/styles/themes/light.ts @@ -7,6 +7,7 @@ const lightTheme = { medium: 24, }, colors: { + splash: "#812228", primary1: "#f2e6e8", primary2: "#dfc2c8", primary3: "#c6919c", diff --git a/utils/retry.ts b/utils/retry.ts new file mode 100644 index 0000000..3d727bd --- /dev/null +++ b/utils/retry.ts @@ -0,0 +1,20 @@ +export const retry = async ( + fn: () => Promise, + attempts = 3, + delayMs = 500, +): Promise => { + let lastError: unknown; + + for (let i = 0; i < attempts; i += 1) { + try { + return await fn(); + } catch (e) { + lastError = e; + if (i < attempts - 1) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + } + + throw lastError; +};