diff --git a/README.md b/README.md index 0a3ead2..2cb9c12 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -react-native-ease by App & Flow +Native Animations with React Native Ease, maintained by App & Flow Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead. diff --git a/docs/docs/getting-started.mdx b/docs/docs/getting-started.mdx index a8f5d31..c8961c9 100644 --- a/docs/docs/getting-started.mdx +++ b/docs/docs/getting-started.mdx @@ -4,7 +4,7 @@ title: Getting started slug: / --- -![react-native-ease hero](https://github.com/user-attachments/assets/8006ed51-d373-4c97-9e80-9937eb9a569e) +![Native Animations with React Native Ease](/img/github-banner.png) Lightweight declarative animations powered by platform APIs. Uses Core Animation on iOS and Animator on Android — zero JS overhead. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 98eb5bf..eb72e0f 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -5,7 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { title: 'React Native Ease', tagline: 'Lightweight declarative animations powered by platform APIs', - favicon: 'img/favicon.png', + favicon: 'img/app-icon.png', url: 'https://appandflow.github.io', baseUrl: '/react-native-ease/', organizationName: 'AppAndFlow', @@ -21,7 +21,7 @@ const config: Config = { 'classic', { docs: { - routeBasePath: '/', + routeBasePath: '/docs', sidebarPath: './sidebars.ts', editUrl: 'https://github.com/AppAndFlow/react-native-ease/tree/main/docs/', @@ -33,7 +33,7 @@ const config: Config = { ], ], themeConfig: { - image: 'img/opengraph-image.png', + image: 'img/github-banner.png', colorMode: { defaultMode: 'light', disableSwitch: false, diff --git a/docs/src/components/landing/CopyInstallCommand.tsx b/docs/src/components/landing/CopyInstallCommand.tsx new file mode 100644 index 0000000..213fb39 --- /dev/null +++ b/docs/src/components/landing/CopyInstallCommand.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styles from '../../css/landing.module.css'; +import Illustration from './Illustration'; + +export default function CopyInstallCommand({ + command, +}: { + command: string; +}): React.ReactElement { + const [status, setStatus] = useState(''); + const copied = status === 'Copied'; + const timer = useRef | undefined>(undefined); + useEffect(() => () => clearTimeout(timer.current), []); + + async function copy() { + clearTimeout(timer.current); + try { + await navigator.clipboard.writeText(command); + setStatus('Copied'); + timer.current = setTimeout(() => setStatus(''), 2000); + } catch { + setStatus('Select the command and copy it manually.'); + } + } + + return ( +
+
+ {command} + +
+ + {status} + +
+ ); +} diff --git a/docs/src/components/landing/FeatureList.tsx b/docs/src/components/landing/FeatureList.tsx new file mode 100644 index 0000000..3ce6620 --- /dev/null +++ b/docs/src/components/landing/FeatureList.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import styles from '../../css/landing.module.css'; +import Link from '@docusaurus/Link'; +import Illustration from './Illustration'; + +import { features } from '../../landing/content'; + +export default function FeatureList(): React.ReactElement { + return ( +
+
+ {features.map(({ title, description, to, icon }) => ( +
+

+ + + {title} + +

+

{description}

+
+ ))} +
+
+ ); +} diff --git a/docs/src/components/landing/Footer.tsx b/docs/src/components/landing/Footer.tsx new file mode 100644 index 0000000..028ac9d --- /dev/null +++ b/docs/src/components/landing/Footer.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from '../../css/landing.module.css'; +import Link from '@docusaurus/Link'; +import { github } from '../../landing/content'; +export default function Footer(): React.ReactElement { + return ( + + ); +} diff --git a/docs/src/components/landing/Hero.tsx b/docs/src/components/landing/Hero.tsx new file mode 100644 index 0000000..edd6c5a --- /dev/null +++ b/docs/src/components/landing/Hero.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import styles from '../../css/landing.module.css'; +import Link from '@docusaurus/Link'; +import CopyInstallCommand from './CopyInstallCommand'; +import { pageTitle, blurb, command } from '../../landing/content'; +export default function Hero(): React.ReactElement { + return ( +
+

{pageTitle}

+

{blurb}

+
+ + Get started + + +
+
+ ); +} diff --git a/docs/src/components/landing/Illustration.tsx b/docs/src/components/landing/Illustration.tsx new file mode 100644 index 0000000..db08b5b --- /dev/null +++ b/docs/src/components/landing/Illustration.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function Illustration({ + file, + alt = '', + ...props +}: Omit, 'src'> & { file: string }) { + return {alt}; +} diff --git a/docs/src/components/landing/LandingLayout.tsx b/docs/src/components/landing/LandingLayout.tsx new file mode 100644 index 0000000..00549f8 --- /dev/null +++ b/docs/src/components/landing/LandingLayout.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styles from '../../css/landing.module.css'; +import LayoutProvider from '@theme/Layout/Provider'; +import SkipToContent from '@theme/SkipToContent'; +import { + PageMetadata, + SkipToContentFallbackId, +} from '@docusaurus/theme-common'; +import Navigation from './Navigation'; +import Footer from './Footer'; +import { pageTitle, blurb } from '../../landing/content'; + +export default function LandingLayout({ + children, +}: { + children: React.ReactNode; +}): React.ReactElement { + return ( + + + +
+ + + ); +} diff --git a/docs/src/components/landing/Navigation.tsx b/docs/src/components/landing/Navigation.tsx new file mode 100644 index 0000000..33b5ce3 --- /dev/null +++ b/docs/src/components/landing/Navigation.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import styles from '../../css/landing.module.css'; +import Link from '@docusaurus/Link'; +import Illustration from './Illustration'; +import { github, productName } from '../../landing/content'; +import packageJson from '../../../../package.json'; + +export default function Navigation(): React.ReactElement { + return ( + + ); +} diff --git a/docs/src/components/landing/PhoneIllustration.tsx b/docs/src/components/landing/PhoneIllustration.tsx new file mode 100644 index 0000000..d577081 --- /dev/null +++ b/docs/src/components/landing/PhoneIllustration.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styles from '../../css/landing.module.css'; +import Illustration from './Illustration'; +import RNLogo from './RNLogo'; +import TopIsland from './TopIsland'; +type IntroPhase = 'logo' | 'island' | 'numbers' | 'sheet' | 'done'; + +export default function PhoneIllustration(): React.ReactElement { + const [sheetLoaded, setSheetLoaded] = useState(false); + const sheetContainer = useRef(null); + const [introPhase, setIntroPhase] = useState('logo'); + const [introStarted, setIntroStarted] = useState(false); + const phoneContainer = useRef(null); + const showSheet = + sheetLoaded && + (introPhase === 'numbers' || + introPhase === 'sheet' || + introPhase === 'done'); + const handleLogoEntranceComplete = useCallback(() => { + setIntroPhase((phase) => (phase === 'logo' ? 'island' : phase)); + }, []); + const handleIslandExpansionComplete = useCallback(() => { + setIntroPhase((phase) => (phase === 'island' ? 'numbers' : phase)); + }, []); + const handleIslandCountComplete = useCallback(() => { + setIntroPhase((phase) => (phase === 'numbers' ? 'sheet' : phase)); + }, []); + useEffect(() => { + if (introStarted) return; + const phone = phoneContainer.current; + if (!phone) return; + const desktop = window.matchMedia('(min-width: 801px)'); + const observer = new IntersectionObserver( + ([entry]) => { + if (entry && entry.intersectionRatio >= 0.5) { + setIntroStarted(true); + observer.disconnect(); + } + }, + { threshold: 0.5 }, + ); + const updateTrigger = () => { + if (desktop.matches) { + setIntroStarted(true); + observer.disconnect(); + } else { + observer.observe(phone); + } + }; + updateTrigger(); + desktop.addEventListener('change', updateTrigger); + return () => { + observer.disconnect(); + desktop.removeEventListener('change', updateTrigger); + }; + }, [introStarted]); + useEffect(() => { + const motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + const skipIntro = () => { + if (motion.matches) setIntroPhase('done'); + }; + skipIntro(); + motion.addEventListener('change', skipIntro); + return () => motion.removeEventListener('change', skipIntro); + }, []); + useEffect(() => { + // Cached images can finish loading before React attaches the load handler. + const image = sheetContainer.current?.querySelector('img'); + if (image?.complete && image.naturalWidth > 0) { + setSheetLoaded(true); + } + }, []); + return ( +
+ + + +
+ setSheetLoaded(true)} + className={[ + styles.bottomSheet, + showSheet ? styles.bottomSheetEnter : undefined, + ] + .filter(Boolean) + .join(' ')} + onAnimationEnd={(event) => { + if (event.target === event.currentTarget) { + setIntroPhase((phase) => (phase === 'sheet' ? 'done' : phase)); + } + }} + data-visible={showSheet} + /> +
+
+ ); +} diff --git a/docs/src/components/landing/RNLogo.tsx b/docs/src/components/landing/RNLogo.tsx new file mode 100644 index 0000000..efe9e16 --- /dev/null +++ b/docs/src/components/landing/RNLogo.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styles from '../../css/landing.module.css'; +import Illustration from './Illustration'; + +type RNLogoProps = { + active: boolean; + onEntranceComplete: () => void; +}; + +export default function RNLogo({ + active, + onEntranceComplete, +}: RNLogoProps): React.ReactElement { + const [logoLoaded, setLogoLoaded] = useState(false); + const [circleLoaded, setCircleLoaded] = useState(false); + const logoContainer = useRef(null); + const logoReady = logoLoaded && circleLoaded && active; + + useEffect(() => { + // Cached images can finish loading before React attaches the load handlers. + const logo = + logoContainer.current?.querySelector( + 'img[alt="React"]', + ); + if (logo?.complete && logo.naturalWidth > 0) setLogoLoaded(true); + const circle = logoContainer.current?.querySelector( + 'img[src$="rn-circle.png"]', + ); + if (circle?.complete && circle.naturalWidth > 0) setCircleLoaded(true); + }, []); + + return ( +
+ setCircleLoaded(true)} + width={830} + height={830} + data-visible={logoReady} + /> + setLogoLoaded(true)} + onAnimationEnd={(event) => { + if (event.target === event.currentTarget) { + onEntranceComplete(); + } + }} + alt="React" + width={409} + height={368} + data-visible={logoReady} + /> +
+ ); +} diff --git a/docs/src/components/landing/TopIsland.tsx b/docs/src/components/landing/TopIsland.tsx new file mode 100644 index 0000000..1176712 --- /dev/null +++ b/docs/src/components/landing/TopIsland.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styles from '../../css/landing.module.css'; +import Illustration from './Illustration'; + +type TopIslandProps = { + active: boolean; + onExpansionComplete: () => void; + onCountComplete: () => void; +}; + +export default function TopIsland({ + active, + onExpansionComplete, + onCountComplete, +}: TopIslandProps): React.ReactElement { + const [islandLoaded, setIslandLoaded] = useState(false); + const islandContainer = useRef(null); + const [expanded, setExpanded] = useState(false); + const [reducedMotion, setReducedMotion] = useState(false); + const [islandNumbers, setIslandNumbers] = useState({ size: '0', fps: '0' }); + const showIsland = islandLoaded && active; + + useEffect(() => { + // Cached images can load before React attaches the load handler. + const image = islandContainer.current?.querySelector('img'); + if (image?.complete && image.naturalWidth > 0) setIslandLoaded(true); + const motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + const updateMotion = () => setReducedMotion(motion.matches); + updateMotion(); + motion.addEventListener('change', updateMotion); + return () => motion.removeEventListener('change', updateMotion); + }, []); + + useEffect(() => { + if (reducedMotion) { + setIslandNumbers({ size: '2', fps: '120' }); + return; + } + if (!expanded) return; + const startedAt = performance.now(); + const timer = window.setInterval(() => { + const progress = Math.min((performance.now() - startedAt) / 600, 1); + const eased = 1 - Math.pow(1 - progress, 3); + setIslandNumbers({ + size: String(Math.round(2 * eased)), + fps: String(Math.round(120 * eased)), + }); + if (progress === 1) { + window.clearInterval(timer); + onCountComplete(); + } + }, 30); + return () => window.clearInterval(timer); + }, [expanded, reducedMotion, onCountComplete]); + + return ( +
{ + if (event.target === event.currentTarget) { + setExpanded(true); + onExpansionComplete(); + } + }} + data-visible={showIsland} + > + setIslandLoaded(true)} + className={styles.responsiveImage} + /> +
+
+
0%
+
+ JS Thread +
+ Utilization +
+
+
+
+ +
+
Bundle Size impact
+
+
+
+ + +
+
+ Frame rate +
+ (per second) +
+
+
+
+ ); +} diff --git a/docs/src/css/landing.module.css b/docs/src/css/landing.module.css new file mode 100644 index 0000000..770d5a4 --- /dev/null +++ b/docs/src/css/landing.module.css @@ -0,0 +1,670 @@ +.page { + position: relative; + overflow: clip; + min-height: 100vh; + background: #fff; + color: #121212; + font-family: 'Ease Inter', sans-serif; + font-weight: 500; +} + +.pageGuide { + position: absolute; + left: max(12px, calc(var(--ease-inset) - 30px)); + top: 0; + bottom: 0; + width: 1px; + background: #f0f0f0; + pointer-events: none; +} + +.grid { + padding: 0 var(--ease-inset); +} + +.navigation { + position: relative; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 16px; + min-height: 103px; + padding: 24px var(--ease-inset); + border-bottom: 1px solid #f0f0f0; + font-size: 12px; + line-height: 1.4; +} + +.mark { + position: absolute; + left: calc(var(--ease-inset) / 2 - 27px); + display: flex; +} + +.productName, +.productName:hover { + color: #828282; + white-space: nowrap; +} + +.githubLink, +.githubLink:hover { + color: #828282; + white-space: nowrap; + display: inline-flex; + align-items: center; + gap: 5px; +} + +.navLink, +.navLink:hover { + color: #828282; + white-space: nowrap; +} + +.hero { + grid-area: hero; + position: relative; + min-height: 329px; + display: flex; + flex-direction: column; + align-items: flex-start; + padding-top: 26px; + padding-bottom: 32px; +} + +.title { + margin: 0 0 16px; + font-family: 'GT Maru', 'Ease Inter', sans-serif; + font-size: clamp(28px, 3vw, 36px); + font-weight: 700; + line-height: 1.1; + letter-spacing: -1.44px; +} + +.blurb { + margin: 0 0 32px; + max-width: 456px; + font-size: 16px; + color: #828282; + line-height: 1.5; + letter-spacing: -0.16px; +} + +.heroActions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + margin-top: auto; +} + +.getStarted, +.getStarted:hover { + padding: 8px 16px; + border-radius: 40px; + background: #121212; + color: #fff; + font-size: 12px; + line-height: 1.4; +} + +.footer { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 22px var(--ease-inset) 26px; + border-top: 1px dashed #ededed; + font-size: 12px; + line-height: 1.4; + color: #828282; +} + +.attributionLink { + color: #008cff; + text-decoration: underline; +} + +.licenseLink, +.licenseLink:hover { + color: #828282; +} + +.phone { + grid-area: phone; + z-index: 1; + max-width: 336px; + align-self: start; + margin-left: 0; + margin-right: 0; +} + +.responsiveImage { + display: block; + width: 100%; + height: auto; +} + +.sheetClip { + position: absolute; + inset: 1% 2.4%; + overflow: hidden; + border-radius: 17% / 7%; + pointer-events: none; +} + +.bottomSheet { + position: absolute; + left: 3.3%; + bottom: 3.5%; + width: 93.4%; + height: auto; +} + +.logoGroup { + position: absolute; + left: 50%; + top: 39.65%; + width: 82.53%; + aspect-ratio: 1; + transform: translate(-50%, -50%); + pointer-events: none; +} + +.circleImage { + display: block; + width: 100%; + height: auto; +} + +.logoImage { + position: absolute; + left: 50%; + top: 50%; + width: 49.28%; + height: auto; + transform: translate(-50%, -50%); +} + +.island { + position: absolute; + left: 5.37%; + top: 2.31%; + width: 88.9%; + aspect-ratio: 894 / 282; + container-type: inline-size; + pointer-events: none; + color: #fff; + line-height: 1.2; + transform-origin: 50% 0%; +} + +.islandStats { + position: absolute; + inset: 0; +} + +.threadStat { + position: absolute; + left: 8.72%; + top: 24.47%; +} + +.threadValue { + font-size: 6.71cqw; + font-weight: 600; + letter-spacing: -0.02em; +} + +.threadLabel { + margin-top: 1.68cqw; + font-size: 3.36cqw; + color: rgba(255, 255, 255, 0.6); + letter-spacing: -0.02em; +} + +.bundleStat { + position: absolute; + left: 31%; + right: 31%; + top: 52.23%; + text-align: center; + letter-spacing: -0.02em; +} + +.bundleValue { + font-size: 4.7cqw; + font-weight: 500; + font-variant-numeric: tabular-nums; +} + +.bundleLabel { + font-size: 2.35cqw; + font-weight: 500; +} + +.fpsStat { + position: absolute; + right: 9.73%; + top: 24.47%; + text-align: right; + letter-spacing: -0.02em; +} + +.fpsValue { + font-size: 6.71cqw; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.fpsUnit { + font-size: 4.03cqw; + margin-left: 0.67cqw; +} + +.fpsLabel { + margin-top: 1.68cqw; + font-size: 3.36cqw; + color: rgba(255, 255, 255, 0.6); +} + +.features { + grid-area: features; + padding-top: 30px; + padding-bottom: 30px; +} + +.featureGrid { + display: grid; + gap: 32px; + max-width: 520px; +} + +.featureTitle { + margin: 0 0 8px; + font-size: 16px; + font-family: inherit; + font-weight: 600; + letter-spacing: -0.32px; + line-height: 24px; +} + +.featureLink, +.featureLink:hover { + display: flex; + align-items: center; + gap: 4px; + color: #121212; +} + +.featureDescription { + margin: 0; + color: #828282; + font-size: 16px; + font-weight: 500; + line-height: 1.5; + letter-spacing: -0.16px; +} + +.install { + position: relative; + min-width: 0; +} + +.installRow { + display: flex; + align-items: center; + gap: 8px; +} + +.command { + padding: 0; + border: 0; + background: transparent; + color: #828282; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + overflow-wrap: anywhere; +} + +.copyButton { + display: grid; + place-items: center; + flex-shrink: 0; + width: 32px; + height: 32px; + padding: 8px; + border: 0; + background: transparent; + cursor: pointer; + color: #008cff; +} + +.copyIcons { + display: grid; + width: 16px; + height: 16px; +} + +.copyIcon { + grid-area: 1 / 1; + place-self: center; + transition: all var(--ifm-transition-fast) ease; +} + +.checkIcon { + grid-area: 1 / 1; + color: #00d600; + transition: all var(--ifm-transition-fast) ease; +} + +@font-face { + font-family: 'Ease Inter'; + src: url('../../static/fonts/InterVariable.woff2') format('woff2'); + font-weight: 100 900; + font-display: swap; +} +@font-face { + font-family: 'GT Maru'; + src: url('../../static/fonts/GT-Maru-Bold.ttf') format('truetype'); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('../../static/fonts/JetBrainsMono-Regular.woff2') format('woff2'); + font-weight: 400; + font-display: swap; +} +.page { + --ease-inset: clamp(24px, 8.625vw, 138px); + --ifm-heading-color: #121212; +} +.grid { + display: grid; + grid-template-columns: minmax(0, 520px) minmax(335px, 1fr); + grid-template-areas: 'hero phone' 'features phone'; + column-gap: 60px; +} +.hero::after { + content: ''; + position: absolute; + left: calc(-1 * var(--ease-inset)); + right: calc(-100vw + var(--ease-inset) + 100%); + bottom: 0; + height: 1px; + background: #f0f0f0; + pointer-events: none; +} +.phone { + position: relative; + width: 100%; + margin-top: -38px; + margin-bottom: 23px; + justify-self: center; + transform: translateX(62px); +} +@keyframes ease-sheet-up { + from { + transform: translateY(calc(100% + 40px)); + } + to { + transform: translateY(0); + } +} +.bottomSheetEnter { + animation: ease-sheet-up 700ms cubic-bezier(0.22, 1, 0.36, 1) both; +} +.page a:hover { + text-decoration: underline; + text-underline-offset: 4px; +} +.page :focus-visible { + outline: 2px solid #008cff; + outline-offset: 5px; + border-radius: 3px; +} +.copyButton { + transition: transform 200ms ease; +} +.page .getStarted { + text-decoration: none; + transition: transform 150ms ease; +} +.page .getStarted:hover { + text-decoration: none; +} +.getStarted:active { + transform: scale(0.96); +} +@media (hover: hover) { + .copyButton:hover { + transform: scale(1.15); + } +} +@media (prefers-reduced-motion: reduce) { + .bottomSheetEnter { + animation: none; + } + .copyButton { + transition: none; + } + .page .getStarted { + transition: none; + } +} +@media (min-width: 801px) { + .phone { + position: sticky; + top: 24px; + width: min(100%, calc((100dvh - 48px) * 336 / 780)); + } +} +@media (max-width: 1100px) { + .grid { + column-gap: 32px; + grid-template-columns: minmax(0, 1fr) 300px; + } + .phone { + transform: none; + } +} +@media (max-width: 800px) { + .page { + --ease-inset: 23px; + } + .grid { + grid-template-columns: minmax(0, 1fr); + grid-template-areas: 'hero' 'phone' 'features'; + } + .phone { + width: 210px; + margin: 24px auto 32px; + } + .navigation { + display: grid; + grid-template-columns: 45px max-content max-content 1fr; + height: 53px; + min-height: 0; + margin-top: 56px; + border-top: 1px solid #f0f0f0; + padding: 0 23px 0 0; + gap: 0 16px; + } + .mark { + position: static; + grid-row: 1; + justify-content: center; + align-self: stretch; + align-items: center; + border-right: 1px solid #f0f0f0; + } + .mark img { + width: 12px; + height: 12px; + } + .productName { + display: none; + } + .pageGuide { + display: none; + } + .hero { + min-height: auto; + padding-top: 32px; + padding-bottom: 0; + } + .hero h1 { + font-size: 24px; + line-height: 1.2; + letter-spacing: -0.96px; + } + .hero > p { + margin-bottom: 0; + } + .heroActions { + flex-direction: column; + align-items: flex-start; + gap: 16px; + margin-top: 16px; + } + .hero::after { + display: none; + } + .features { + position: relative; + padding-top: 40px; + } + .features::before { + content: ''; + position: absolute; + top: 0; + left: calc(-1 * var(--ease-inset)); + right: calc(-1 * var(--ease-inset)); + border-top: 1px solid #f0f0f0; + } + .features > div { + gap: 48px; + } +} + +@keyframes ease-circle-spin { + to { + transform: rotate(360deg); + } +} +@keyframes ease-circle-reveal { + from { + opacity: 0; + scale: 0.8; + } + to { + opacity: 1; + scale: 1; + } +} +.rnCircle { + animation: ease-circle-reveal 600ms cubic-bezier(0.22, 1, 0.36, 1) 400ms both, + ease-circle-spin 20s linear infinite; +} +@keyframes ease-logo-enter { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.8); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} +.rnLogo { + animation: ease-logo-enter 600ms cubic-bezier(0.22, 1, 0.36, 1) 400ms both; +} +@media (prefers-reduced-motion: reduce) { + .rnCircle, + .rnLogo { + animation: none; + } +} + +@keyframes ease-island-expand { + 0% { + clip-path: inset(5.3% 35.1% 67% 34.6% round 999px); + transform: scale(1); + } + 65% { + clip-path: inset(0 round 13.1% / 41.5%); + transform: scale(1.035, 1.08); + } + 82% { + clip-path: inset(0 round 13.1% / 41.5%); + transform: scale(0.99, 0.97); + } + 100% { + clip-path: inset(0 round 13.1% / 41.5%); + transform: scale(1); + } +} +@keyframes ease-island-reveal { + from { + opacity: 0; + filter: blur(5px); + } + to { + opacity: 1; + filter: blur(0); + } +} +.islandEnter { + animation: ease-island-expand 600ms cubic-bezier(0.22, 1, 0.36, 1) 200ms both; +} +.islandEnter .islandStats { + animation: ease-island-reveal 300ms ease-out 550ms both; +} +@media (prefers-reduced-motion: reduce) { + .islandEnter, + .islandEnter .islandStats { + animation: none; + } +} + +/* Visibility and clipboard states are driven by component state. */ +.island[data-visible='false'], +.circleImage[data-visible='false'], +.logoImage[data-visible='false'], +.bottomSheet[data-visible='false'] { + visibility: hidden; +} +.copyIcon { + opacity: 1; + transform: scale(1); +} +.checkIcon { + opacity: 0; + transform: scale(0.33); + transition-delay: 0ms; +} +.install[data-copied='true'] .copyIcon { + opacity: 0; + transform: scale(0.33); +} +.install[data-copied='true'] .checkIcon { + opacity: 1; + transform: scale(1); + transition-delay: 75ms; +} +.srOnly { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} +.copyStatus { + position: absolute; + top: 100%; + left: 0; + font-size: 11px; + color: #59677f; +} diff --git a/docs/src/landing/content.ts b/docs/src/landing/content.ts new file mode 100644 index 0000000..a8d4d6d --- /dev/null +++ b/docs/src/landing/content.ts @@ -0,0 +1,30 @@ +export const github = 'https://github.com/AppAndFlow/react-native-ease'; +export const productName = 'react-native-ease'; +export const pageTitle = 'Native Animations'; +export const blurb = + 'Give EaseView your target values and a transition, and let iOS and Android handle the animation – all from the React state you already have.'; +export const command = 'npm install react-native-ease'; + +export const features = [ + { + title: 'An API that feels like React', + description: + 'Set your target values with animate and control how they change with transition. EaseView takes children, styles, and standard view props, so you can use it like any other component.', + to: '/docs/usage', + icon: 'react.svg', + }, + { + title: 'Native animation, no JS loop', + description: + 'Ease uses Core Animation on iOS and ObjectAnimator and SpringAnimation on Android. Once an animation starts, the native APIs handle the updates instead of a JavaScript animation loop.', + to: '/docs/how-it-works', + icon: 'phone.svg', + }, + { + title: 'Pick the transition you need', + description: + 'Use an easing curve for a fade, a spring for movement, or different transitions for different properties. Set the duration, damping, stiffness, and delay when you need to fine-tune an animation.', + to: '/docs/usage#per-property-transitions', + icon: 'transition.svg', + }, +]; diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx new file mode 100644 index 0000000..aaf528a --- /dev/null +++ b/docs/src/pages/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import LandingLayout from '../components/landing/LandingLayout'; +import Hero from '../components/landing/Hero'; +import PhoneIllustration from '../components/landing/PhoneIllustration'; +import FeatureList from '../components/landing/FeatureList'; + +export default function Home(): React.ReactElement { + return ( + + + + + + ); +} diff --git a/docs/static/fonts/GT-Maru-Bold.ttf b/docs/static/fonts/GT-Maru-Bold.ttf new file mode 100644 index 0000000..59871bd Binary files /dev/null and b/docs/static/fonts/GT-Maru-Bold.ttf differ diff --git a/docs/static/fonts/Inter-LICENSE.txt b/docs/static/fonts/Inter-LICENSE.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/docs/static/fonts/Inter-LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/static/fonts/InterVariable.woff2 b/docs/static/fonts/InterVariable.woff2 new file mode 100644 index 0000000..5a8d3e7 Binary files /dev/null and b/docs/static/fonts/InterVariable.woff2 differ diff --git a/docs/static/fonts/JetBrainsMono-OFL.txt b/docs/static/fonts/JetBrainsMono-OFL.txt new file mode 100644 index 0000000..5ceee00 --- /dev/null +++ b/docs/static/fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/static/fonts/JetBrainsMono-Regular.woff2 b/docs/static/fonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 0000000..66c5467 Binary files /dev/null and b/docs/static/fonts/JetBrainsMono-Regular.woff2 differ diff --git a/docs/static/img/app-icon.png b/docs/static/img/app-icon.png index e93ef0d..9be4ef9 100644 Binary files a/docs/static/img/app-icon.png and b/docs/static/img/app-icon.png differ diff --git a/docs/static/img/github-banner.png b/docs/static/img/github-banner.png new file mode 100644 index 0000000..eed88e9 Binary files /dev/null and b/docs/static/img/github-banner.png differ diff --git a/docs/static/img/landing/bottom-sheet.png b/docs/static/img/landing/bottom-sheet.png new file mode 100644 index 0000000..37e52e3 Binary files /dev/null and b/docs/static/img/landing/bottom-sheet.png differ diff --git a/docs/static/img/landing/copy.svg b/docs/static/img/landing/copy.svg new file mode 100644 index 0000000..97de2f9 --- /dev/null +++ b/docs/static/img/landing/copy.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/static/img/landing/github.svg b/docs/static/img/landing/github.svg new file mode 100644 index 0000000..f4ecb52 --- /dev/null +++ b/docs/static/img/landing/github.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/static/img/landing/ios-island.png b/docs/static/img/landing/ios-island.png new file mode 100644 index 0000000..0d6290d Binary files /dev/null and b/docs/static/img/landing/ios-island.png differ diff --git a/docs/static/img/landing/mark.svg b/docs/static/img/landing/mark.svg new file mode 100644 index 0000000..d1f04ef --- /dev/null +++ b/docs/static/img/landing/mark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/static/img/landing/phone.png b/docs/static/img/landing/phone.png new file mode 100644 index 0000000..f020d8f Binary files /dev/null and b/docs/static/img/landing/phone.png differ diff --git a/docs/static/img/landing/phone.svg b/docs/static/img/landing/phone.svg new file mode 100644 index 0000000..c45e92a --- /dev/null +++ b/docs/static/img/landing/phone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/static/img/landing/react.svg b/docs/static/img/landing/react.svg new file mode 100644 index 0000000..88f64ed --- /dev/null +++ b/docs/static/img/landing/react.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/static/img/landing/rn-circle.png b/docs/static/img/landing/rn-circle.png new file mode 100644 index 0000000..cffc694 Binary files /dev/null and b/docs/static/img/landing/rn-circle.png differ diff --git a/docs/static/img/landing/rn-logo.png b/docs/static/img/landing/rn-logo.png new file mode 100644 index 0000000..21451ef Binary files /dev/null and b/docs/static/img/landing/rn-logo.png differ diff --git a/docs/static/img/landing/transition.svg b/docs/static/img/landing/transition.svg new file mode 100644 index 0000000..401ca88 --- /dev/null +++ b/docs/static/img/landing/transition.svg @@ -0,0 +1,5 @@ + + + + +