├── .gitignore ├── .gitmodules ├── .pnpmfile.cjs ├── LICENSE ├── README.md ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── website ├── .dockerignore ├── .gitignore ├── Dockerfile ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── .keep └── og.jpeg ├── sentry.client.config.js ├── sentry.server.config.js ├── src ├── pages │ ├── _app.tsx │ ├── _error.tsx │ ├── home.tsx │ └── index.tsx └── styles │ ├── globals.css │ └── index.css ├── tailwind.config.cjs ├── tsconfig.json └── vitest.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "beskar"] 2 | path = beskar 3 | url = https://github.com/remorses/beskar 4 | -------------------------------------------------------------------------------- /.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | let enforceSingleVersion = [ 2 | 'react-hot-toast', // 3 | 'react', 4 | 'react-dom', 5 | // 'next-auth', 6 | // 'next', 7 | 'date-fns', 8 | // 'styled-jsx', 9 | 'nprogress', 10 | 'tailwindcss', 11 | '@chakra-ui/react', 12 | '@headlessui/react', 13 | // 'html-dom-parser', 14 | ] 15 | 16 | // enforceSingleVersion = [] 17 | 18 | function afterAllResolved(lockfile, context) { 19 | console.log(`Checking duplicate packages`) 20 | const packagesKeys = Object.keys(lockfile.packages) 21 | const found = {} 22 | for (let p of packagesKeys) { 23 | for (let x of enforceSingleVersion) { 24 | if (p.startsWith(`/${x}/`)) { 25 | if (found[x]) { 26 | found[x] += 1 27 | } else { 28 | found[x] = 1 29 | } 30 | } 31 | } 32 | } 33 | let msg = '' 34 | for (let p in found) { 35 | const count = found[p] 36 | if (count > 1) { 37 | msg += `${p} found ${count} times\n` 38 | msg += explainProblemInDuplicateDep(p, lockfile) 39 | } 40 | } 41 | if (msg) { 42 | throw new Error(msg) 43 | } 44 | return lockfile 45 | } 46 | 47 | function explainProblemInDuplicateDep(package, lockfile) { 48 | const packagesKeys = Object.keys(lockfile.packages) 49 | let found = {} 50 | for (let p of packagesKeys) { 51 | if (p.startsWith(`/${package}/`)) { 52 | const config = lockfile.packages[p] 53 | found[p] = Object.keys(config.dependencies || {}).map( 54 | (k) => `${k}@${config.dependencies[k]}`, 55 | ) 56 | } 57 | } 58 | 59 | const differences = getDifferences(Object.values(found)) 60 | 61 | if (differences.length) { 62 | return `${package} has different set of dependencies:\n${JSON.stringify( 63 | differences, 64 | null, 65 | 2, 66 | )}` 67 | } 68 | return '' 69 | } 70 | 71 | // return different items from a list of arrays of strings 72 | function getDifferences(arrays) { 73 | const result = {} 74 | for (let a of arrays) { 75 | for (let x of a) { 76 | if (result[x]) { 77 | result[x] += 1 78 | } else { 79 | result[x] = 1 80 | } 81 | } 82 | } 83 | for (let x in result) { 84 | if (result[x] === arrays.length) { 85 | delete result[x] 86 | } 87 | } 88 | return Object.keys(result) 89 | } 90 | 91 | module.exports = { 92 | hooks: { 93 | afterAllResolved, 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tommaso De Rossi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # framer-motion-visualizer 2 | Visualize framer-motion spring animations 3 | 4 | 5 | https://twitter.com/__morse/status/1622197467633618945?s=20&t=D7VYT1d5vCqr04crZ-h6WQ 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - website 3 | - beskar 4 | -------------------------------------------------------------------------------- /website/.dockerignore: -------------------------------------------------------------------------------- 1 | .env* 2 | README.md 3 | # ignore prisma because it's a native dependency and we need to install it inside docker 4 | **/@prisma* 5 | **/node_modules/canvas 6 | **/node_modules/.prisma 7 | **/node_modules/@prisma 8 | **/*.env* 9 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # vercel 28 | .vercel 29 | 30 | # typescript 31 | *.tsbuildinfo 32 | 33 | # Sentry 34 | .sentryclirc 35 | -------------------------------------------------------------------------------- /website/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-slim AS runner 2 | RUN apt update && apt install -y curl tree 3 | 4 | WORKDIR /app 5 | 6 | 7 | # install prisma 8 | ENV PRISMA_FILE=./.next/schema.prisma 9 | ENV PRISMA_VERSION=4.4.0 10 | RUN apt update && apt install openssl ca-certificates -y 11 | RUN npm i -g prisma@$PRISMA_VERSION prisma-generator-kysely-mysql@latest prisma-json-schema-generator 12 | RUN npm init -y && npm install @prisma/client@$PRISMA_VERSION 13 | COPY $PRISMA_FILE ./schema.prisma 14 | RUN prisma generate --schema ./schema.prisma 15 | 16 | ENV FOLDER website 17 | ENV NODE_ENV production 18 | 19 | ADD ./.next/standalone ./ 20 | 21 | WORKDIR /app/$FOLDER 22 | 23 | 24 | COPY ./.next/static ./.next/static 25 | COPY ./next.config.js ./ 26 | COPY ./public ./public 27 | COPY ./package.json ./ 28 | 29 | EXPOSE 3000 30 | 31 | ENV PORT 3000 32 | 33 | # CMD tree ./ 34 | CMD node server.js 35 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const { withSuperjson } = require('next-superjson') 4 | // const withBundleAnalyzer = require('@next/bundle-analyzer')({ 5 | // enabled: Boolean(process.env.ANALYZE), 6 | // }) 7 | 8 | const { withSentryConfig } = require('@sentry/nextjs') 9 | 10 | const withRpc = require('next-rpc')({ 11 | experimentalContext: true, 12 | }) 13 | 14 | const piped = pipe( 15 | withRpc, 16 | withSuperjson(), 17 | // (c) => 18 | // withSentryConfig(c, { 19 | // org: 'salespack', 20 | // project: 'website', 21 | // dryRun: process.env.NODE_ENV === 'development', 22 | // silent: true, // 23 | // }), 24 | ) 25 | 26 | const isPreview = 27 | process.env.NODE_ENV === 'development' || 28 | process.env.NEXT_PUBLIC_ENV === 'preview' 29 | 30 | /** @type {import('next').NextConfig} */ 31 | const config = { 32 | reactStrictMode: false, 33 | productionBrowserSourceMaps: true, 34 | output: 'standalone', 35 | outputFileTracing: true, 36 | experimental: { 37 | externalDir: true, 38 | externalDir: true, 39 | outputFileTracingRoot: path.join(__dirname, '../'), 40 | }, 41 | transpilePackages: ['beskar'], 42 | swcMinify: true, 43 | eslint: { 44 | // Warning: This allows production builds to successfully complete even if 45 | // your project has ESLint errors. 46 | ignoreDuringBuilds: process.env.NODE_ENV !== 'production', 47 | }, 48 | webpack: (config, { isServer, dev: isDev }) => { 49 | if (isPreview) { 50 | config.resolve.alias = { 51 | ...config.resolve.alias, 52 | 'react-dom$': 'react-dom/profiling', 53 | 'scheduler/tracing': 'scheduler/tracing-profiling', 54 | } 55 | } 56 | config.externals = config.externals.concat([]) 57 | return config 58 | }, 59 | } 60 | 61 | module.exports = piped(config) 62 | 63 | function pipe(...fns) { 64 | return (x) => fns.reduce((v, f) => f(v), x) 65 | } 66 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "private": true, 4 | "scripts": { 5 | "dev": "pnpm next dev --port 6066", 6 | "build": "next build", 7 | "test": "pnpm vitest", 8 | "lint": "eslint src --fix --color" 9 | }, 10 | "dependencies": { 11 | "@formatjs/intl-numberformat": "^8.0.4", 12 | "@heroicons/react": "^1.0.6", 13 | "@sentry/nextjs": "^7.36.0", 14 | "@splitbee/web": "^0.3.0", 15 | "@tremor/react": "^1.7.0", 16 | "@vercel/analytics": "^0.1.8", 17 | "babel-loader": "^8.2.5", 18 | "baby-i-am-faded": "^4.0.14", 19 | "beskar": "workspace:^0.0.0", 20 | "classnames": "^2.3.1", 21 | "colord": "^2.9.2", 22 | "date-fns": "^2.28.0", 23 | "framer-motion": "^9.0.1", 24 | "is-valid-domain": "^0.1.6", 25 | "jotai": "^1.7.6", 26 | "js-cookie": "^3.0.1", 27 | "next": "^13.1.5", 28 | "next-rpc": "^3.5.1", 29 | "next-seo": "^5.5.0", 30 | "next-themes": "^0.2.1", 31 | "nextjs-progressbar": "^0.0.14", 32 | "nodemailer": "^6.7.7", 33 | "nprogress": "*", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0", 36 | "react-hook-form": "^7.36.1", 37 | "react-hot-toast": "^2.2.0", 38 | "superjson": "^1.9.1", 39 | "swr": "^1.3.0", 40 | "uuid": "^8.3.2", 41 | "vite-tsconfig-paths": "^3.5.1" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "17.0.36", 45 | "@types/nodemailer": "^6.4.6", 46 | "@types/react": "18.0.9", 47 | "autoprefixer": "^10.4.7", 48 | "eslint-config-next": "12.3.1", 49 | "next-superjson": "^0.0.4", 50 | "postcss": "^8.4.16", 51 | "typescript": "4.9.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remorses/framer-motion-visualizer/84801382d032ead355e7fe06434f6153a42a4dc7/website/public/.keep -------------------------------------------------------------------------------- /website/public/og.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remorses/framer-motion-visualizer/84801382d032ead355e7fe06434f6153a42a4dc7/website/public/og.jpeg -------------------------------------------------------------------------------- /website/sentry.client.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the browser. 2 | // The config you add here will be used whenever a page is visited. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs' 6 | 7 | Sentry.init({ 8 | dsn: 'https://559f09ce0bde43bb921a66ca543b5c41@o4504578358771712.ingest.sentry.io/4504578359754752', 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1.0, 12 | onFatalError: onUncaughtException, 13 | beforeSend(event) { 14 | // do not send in development 15 | if (process.env.NODE_ENV === 'development') { 16 | return null 17 | } 18 | return event 19 | }, 20 | 21 | // ... 22 | // Note: if you want to override the automatic release value, do not set a 23 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 24 | // that it will also get attached to your source maps 25 | }) 26 | 27 | // https://github.com/nodejs/node/issues/42154 28 | function onUncaughtException(error) { 29 | if (error?.['code'] === 'ECONNRESET') { 30 | console.log(`handled uncaughtException ${error}`) 31 | return 32 | } 33 | console.error('UNCAUGHT EXCEPTION') 34 | console.error(error) 35 | 36 | process.exit(1) 37 | } 38 | -------------------------------------------------------------------------------- /website/sentry.server.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs' 6 | 7 | Sentry.init({ 8 | dsn: 'https://559f09ce0bde43bb921a66ca543b5c41@o4504578358771712.ingest.sentry.io/4504578359754752', 9 | 10 | tracesSampleRate: 1.0, 11 | // onFatalError: onUncaughtException, 12 | beforeSend(event) { 13 | // do not send in development 14 | if (process.env.NODE_ENV === 'development') { 15 | return null 16 | } 17 | return event 18 | }, 19 | integrations(integrations) { 20 | return integrations.filter( 21 | (integration) => integration.id !== 'OnUncaughtException', 22 | ) 23 | }, 24 | }) 25 | 26 | // https://github.com/nodejs/node/issues/42154 27 | global.process.on('uncaughtException', (error) => { 28 | const hub = Sentry.getCurrentHub() 29 | hub.withScope(async (scope) => { 30 | scope.setLevel('fatal') 31 | hub.captureException(error, { originalException: error }) 32 | }) 33 | if (error?.['code'] === 'ECONNRESET') { 34 | console.log(`handled ECONNRESET ${error}`) 35 | return 36 | } 37 | console.error('UNCAUGHT EXCEPTION') 38 | console.error(error) 39 | // console.error(origin) 40 | process.exit(1) 41 | }) 42 | -------------------------------------------------------------------------------- /website/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'baby-i-am-faded/styles.css' 2 | import '@app/styles/index.css' 3 | import '@tremor/react/dist/esm/tremor.css' 4 | import { Analytics } from '@vercel/analytics/react' 5 | 6 | import { ThemeProvider } from 'next-themes' 7 | import NextNprogress from 'nextjs-progressbar' 8 | 9 | import { BeskarProvider } from 'beskar/src/BeskarProvider' 10 | import { useRouter } from 'next/router' 11 | import Script from 'next/script' 12 | import { useEffect } from 'react' 13 | import { Toaster } from 'react-hot-toast' 14 | 15 | function MyApp({ Component, pageProps: { session, ...pageProps } }) { 16 | const router = useRouter() 17 | 18 | return ( 19 | <> 20 | {/* */} 21 | 22 | 23 | 24 | 30 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | export default MyApp 51 | -------------------------------------------------------------------------------- /website/src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher. 3 | * 4 | * NOTE: If using this with `next` version 12.2.0 or lower, uncomment the 5 | * penultimate line in `CustomErrorComponent`. 6 | * 7 | * This page is loaded by Nextjs: 8 | * - on the server, when data-fetching methods throw or reject 9 | * - on the client, when `getInitialProps` throws or rejects 10 | * - on the client, when a React lifecycle method throws or rejects, and it's 11 | * caught by the built-in Nextjs error boundary 12 | * 13 | * See: 14 | * - https://nextjs.org/docs/basic-features/data-fetching/overview 15 | * - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props 16 | * - https://reactjs.org/docs/error-boundaries.html 17 | */ 18 | 19 | import * as Sentry from '@sentry/nextjs'; 20 | import NextErrorComponent from 'next/error'; 21 | 22 | const CustomErrorComponent = props => { 23 | // If you're using a Nextjs version prior to 12.2.1, uncomment this to 24 | // compensate for https://github.com/vercel/next.js/issues/8592 25 | // Sentry.captureUnderscoreErrorException(props); 26 | 27 | return ; 28 | }; 29 | 30 | CustomErrorComponent.getInitialProps = async contextData => { 31 | // In case this is running in a serverless function, await this in order to give Sentry 32 | // time to send the error before the lambda exits 33 | await Sentry.captureUnderscoreErrorException(contextData); 34 | 35 | // This will contain the status code of the response 36 | return NextErrorComponent.getInitialProps(contextData); 37 | }; 38 | 39 | export default CustomErrorComponent; 40 | -------------------------------------------------------------------------------- /website/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import { GetStaticPropsContext } from 'next' 3 | 4 | import { 5 | Card, 6 | Title, 7 | Text, 8 | LineChart, 9 | Toggle, 10 | ToggleItem, 11 | Flex, 12 | Block, 13 | } from '@tremor/react' 14 | 15 | import { BadgeSelect } from 'beskar/src/analytics/components/badge-select' 16 | import { RangeSlider } from 'beskar/src/landing/form' 17 | import { Button } from 'beskar/src/landing/' 18 | // import { Button, Link } from 'beskar/src/landing' 19 | 20 | import classNames from 'classnames' 21 | import { 22 | motion, 23 | useMotionValue, 24 | useAnimationControls, 25 | Spring, 26 | animate, 27 | spring, 28 | } from 'framer-motion' 29 | import React from 'react' 30 | import { NextSeo } from 'next-seo' 31 | 32 | const ogImageUrl = new URL( 33 | require('@app/../public/og.jpeg')?.default.src, 34 | 'https://framer-motion-visualizer.vercel.app', 35 | ).toString() 36 | 37 | function App() { 38 | const [mode, setMode] = useState<'duration' | 'mass'>('mass') 39 | 40 | const initialState = (mode) => 41 | mode === 'duration' 42 | ? { duration: 1, bounce: 0 } 43 | : { mass: 1, damping: 10, stiffness: 100 } 44 | const [state, setState] = useState>(initialState(mode)) 45 | 46 | let playbackRate = useMotionValue(1) 47 | let x = useMotionValue(0) 48 | 49 | const config = 50 | mode === 'duration' 51 | ? { 52 | duration: { 53 | min: 0.1, 54 | max: 10, 55 | step: 0.1, 56 | }, 57 | bounce: { 58 | min: 0, 59 | max: 1, 60 | step: 0.01, 61 | }, 62 | // restSpeed: { 63 | // min: 0, 64 | // max: 10, 65 | // step: 0.1, 66 | // }, 67 | // restDelta: { 68 | // min: 0, 69 | // max: 10, 70 | // step: 0.1, 71 | // }, 72 | } 73 | : ({ 74 | stiffness: { 75 | min: 1, 76 | max: 1000, 77 | step: 1, 78 | }, 79 | damping: { 80 | min: 1, 81 | max: 100, 82 | step: 1, 83 | }, 84 | mass: { 85 | min: 1, 86 | max: 100, 87 | step: 1, 88 | }, 89 | } as const) 90 | 91 | let lastValue = 300 92 | let [count, setCount] = useState(0) 93 | useDebouncedEffect(() => { 94 | let animations = [] 95 | x.jump(0) 96 | 97 | let c = animate(x, lastValue, { 98 | type: 'spring', 99 | // repeat: Infinity, 100 | // repeatType: 'reverse', 101 | ...state, 102 | onComplete: () => {}, 103 | }) 104 | animations.push(c) 105 | return () => { 106 | animations.forEach((a) => a.stop()) 107 | } 108 | }, [count, state]) 109 | let [chartData, setChartData] = useState([]) 110 | useDebouncedEffect( 111 | () => { 112 | let from = 0 113 | 114 | let duration = state.duration ? state.duration * 1000 : 1000 115 | let springAnimation = spring({ 116 | keyframes: [from, lastValue], 117 | ...state, 118 | duration, 119 | }) 120 | let keyframes = [] 121 | let t = 0 122 | let springTimeResolution = 20 123 | let status = { done: false, value: from } 124 | // let maxT = state.duration + 3 || 30 125 | while (!status.done) { 126 | status = springAnimation.next(t) 127 | keyframes.push(status.value) 128 | t += springTimeResolution 129 | } 130 | setChartData( 131 | keyframes.map((v, i) => ({ 132 | time: i * springTimeResolution, 133 | value: v, 134 | })), 135 | ) 136 | }, 137 | [state], 138 | 50, 139 | ) 140 | let size = 40 141 | const container = useRef(null) 142 | 143 | return ( 144 |
145 | 171 | {/*
{JSON.stringify(state, null, 4)}
*/} 172 | {/*
{JSON.stringify(chartData, null, 4)}
*/} 173 |
174 |

175 | Framer Motion Visualizer 176 |

177 |

178 | Visualize Framer Motion Animations 179 |
180 | Made by{' '} 181 | 185 | @__morse 186 | 187 |

188 |
189 | 190 |
191 |
195 | x.toFixed(0) + 'px'} 203 | yAxisWidth='w-12' 204 | showAnimation={false} 205 | height='h-60' 206 | startEndOnly={!!chartData.length} 207 | // showXAxis={false} 208 | // showGridLines={false} 209 | /> 210 | 211 | {/* { 215 | setLoaded(true) 216 | }} 217 | controls 218 | playsInline 219 | autoPlay 220 | muted 221 | loop 222 | /> */} 223 |
224 |
225 | 226 | { 228 | setMode(mode as any) 229 | setState(initialState(mode)) 230 | }} 231 | selected={mode} 232 | options={['mass', 'duration'].map((x) => ({ 233 | value: x, 234 | name: x, 235 | }))} 236 | /> 237 | 238 | {Object.keys(config).map((key) => { 239 | let conf = config[key as keyof typeof config] 240 | let value = state[key as keyof typeof state] || 0 241 | return ( 242 |
243 |
244 |
{key}
245 |
246 |
247 | {value} 248 |
249 |
250 | { 254 | playbackRate.stop() 255 | playbackRate.jump(0) 256 | setState((prev) => { 257 | return { 258 | ...prev, 259 | [key]: Number(e.target.value), 260 | } 261 | }) 262 | }} 263 | step={conf.step} 264 | min={conf.min} 265 | max={conf.max} 266 | /> 267 |
268 | ) 269 | })} 270 |
271 |
272 | 273 |
274 |
278 |
279 | 299 |
300 | 307 |
308 |
309 | ) 310 | } 311 | 312 | const valueFormatterAbsolute = (number: number) => 313 | Intl.NumberFormat('us').format(number).toString() 314 | 315 | export default App 316 | 317 | function useDebouncedEffect(callback, deps = [], delay = 120) { 318 | const data = React.useRef({ 319 | firstTime: true, 320 | clearFunc: null as Function | null, 321 | }) 322 | React.useEffect(() => { 323 | const { firstTime, clearFunc } = data.current 324 | 325 | if (firstTime) { 326 | data.current.firstTime = false 327 | } 328 | 329 | const handler = setTimeout(() => { 330 | if (clearFunc && typeof clearFunc === 'function') { 331 | clearFunc() 332 | } 333 | data.current.clearFunc = callback() 334 | }, delay) 335 | 336 | return () => { 337 | clearTimeout(handler) 338 | } 339 | }, [delay, ...deps]) 340 | } 341 | 342 | export function getStaticProps(ctx: GetStaticPropsContext) { 343 | return { 344 | props: {}, 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Page, { getStaticProps } from './home' 2 | 3 | export { getStaticProps } 4 | export default Page 5 | -------------------------------------------------------------------------------- /website/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --page-max-width: 1200px; 3 | --pagePadding: 20px; 4 | } 5 | 6 | /* Fix all that shitty libraries (like chakra) that try to remove scroll bar and layout shift everything */ 7 | html > body { 8 | margin-right: 0 !important; 9 | } 10 | /* html { 11 | padding-right: 0 !important; 12 | } */ 13 | 14 | * { 15 | box-sizing: border-box; 16 | border-color: theme('colors.gray.200'); 17 | } 18 | .dark * { 19 | border-color: theme('colors.gray.600'); 20 | } 21 | 22 | /* do not zoom on ios select */ 23 | select { 24 | font-size: 16px; 25 | } 26 | 27 | html { 28 | /* min-height: 100%; */ 29 | background-color: theme('colors.gray.100'); 30 | /* height: 100vh; */ 31 | position: relative; 32 | overflow-x: hidden !important; 33 | overflow-y: scroll; 34 | scroll-behavior: smooth; 35 | color: theme('colors.gray.700'); 36 | touch-action: pan-x pan-y pinch-zoom !important; 37 | -webkit-tap-highlight-color: transparent !important; 38 | -webkit-touch-callout: none !important; 39 | } 40 | 41 | html.dark { 42 | background-color: theme('colors.gray.900'); 43 | color: theme('colors.gray.200'); 44 | color-scheme: dark; 45 | } 46 | 47 | #__next { 48 | } 49 | 50 | body { 51 | position: relative; 52 | margin: 0; 53 | scroll-behavior: smooth; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | text-rendering: optimizeLegibility; 57 | } 58 | 59 | .scrollbar-hide::-webkit-scrollbar { 60 | display: none; 61 | } 62 | .scrollbar-hide { 63 | -ms-overflow-style: none; /* IE and Edge */ 64 | scrollbar-width: none; /* Firefox */ 65 | } 66 | -------------------------------------------------------------------------------- /website/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import 'beskar/styles/focus.css'; 6 | @import './globals.css'; 7 | -------------------------------------------------------------------------------- /website/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const colors = require('beskar/colors') 2 | 3 | /** @type {import('tailwindcss/tailwind-config').TailwindConfig} */ 4 | module.exports = { 5 | mode: 'jit', 6 | content: [ 7 | './src/**/*.{js,ts,jsx,tsx}', // 8 | '../beskar/src/**/*.{js,ts,jsx,tsx}', // 9 | ], 10 | darkMode: 'class', 11 | theme: { 12 | extend: { 13 | colors: { 14 | 15 | ...colors, 16 | }, 17 | }, 18 | }, 19 | variants: { 20 | extend: {}, 21 | }, 22 | plugins: [], 23 | } 24 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "downlevelIteration": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "baseUrl": "./", 18 | "paths": { 19 | "@app/*": ["./src/*"] 20 | }, 21 | "incremental": true 22 | }, 23 | "include": ["src", "src/global.ts", "next-env.d.ts", "scripts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /website/vitest.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from 'vite' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | export default defineConfig({ 6 | test: { 7 | exclude: ['**/dist/**', '**/esm/**', '**/node_modules/**', '**/e2e/**'], 8 | 9 | threads: false, 10 | }, 11 | plugins: [tsconfigPaths()], 12 | }) 13 | --------------------------------------------------------------------------------