├── .eslintrc.json ├── .github ├── funding.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── components ├── ActiveLink │ └── ActiveLink.tsx ├── Button │ ├── Button.tsx │ └── styles.module.css ├── Footer │ ├── Footer.tsx │ └── styles.module.css ├── Header │ ├── Header.tsx │ ├── Logo.tsx │ └── styles.module.css ├── HeroButton │ ├── HeroButton.tsx │ └── styles.module.css ├── Layout │ ├── Layout.tsx │ ├── providers.tsx │ └── styles.module.css ├── Markdown │ ├── Markdown.tsx │ └── styles.module.css ├── MetaballVisualization │ ├── Body.ts │ ├── MetaballVisualization.tsx │ ├── MetaballViz.ts │ └── styles.module.css ├── PageHead │ └── PageHead.tsx └── WebGLSupportChecker │ └── WebGLSupportChecker.tsx ├── icons ├── Discord.tsx ├── GitHub.tsx ├── Twitter.tsx └── index.ts ├── lib ├── bootstrap.ts ├── config.ts └── markdown-to-html.ts ├── license ├── next.config.mjs ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── about │ ├── index.tsx │ └── styles.module.css ├── index.module.css └── index.tsx ├── pnpm-lock.yaml ├── postcss.config.cjs ├── public ├── favicon.ico ├── icon.png ├── icon.svg ├── logo-dark.png ├── logo-light.png ├── robots.txt └── social.jpg ├── readme.md ├── store └── metaballs.ts ├── styles └── globals.css ├── tailwind.config.cjs └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [transitive-bullshit] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 19 14 | - 18 15 | - 16 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install pnpm 27 | uses: pnpm/action-setup@v2 28 | id: pnpm-install 29 | with: 30 | version: 7 31 | run_install: false 32 | 33 | - name: Get pnpm store directory 34 | id: pnpm-cache 35 | shell: bash 36 | run: | 37 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 38 | 39 | - uses: actions/cache@v3 40 | name: Setup pnpm cache 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | - name: Install dependencies 48 | run: pnpm install --frozen-lockfile 49 | 50 | - name: Run test 51 | run: pnpm run test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.next/ 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('@trivago/prettier-plugin-sort-imports')], 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | semi: false, 6 | useTabs: false, 7 | tabWidth: 2, 8 | bracketSpacing: true, 9 | bracketSameLine: false, 10 | arrowParens: 'always', 11 | trailingComma: 'none', 12 | importOrder: ['^node:.*', '', '^@/', '^[./]'], 13 | importOrderSeparation: true, 14 | importOrderSortSpecifiers: true, 15 | importOrderGroupNamespaceSpecifiers: true 16 | } 17 | -------------------------------------------------------------------------------- /components/ActiveLink/ActiveLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import cs from 'clsx' 5 | import Link, { LinkProps } from 'next/link' 6 | import { usePathname } from 'next/navigation' 7 | 8 | type ActiveLinkProps = LinkProps & { 9 | children?: React.ReactNode 10 | className?: string 11 | activeClassName?: string 12 | style?: React.CSSProperties 13 | 14 | // optional comparison function to normalize URLs before comparing 15 | compare?: (a?: any, b?: any) => boolean 16 | } 17 | 18 | /** 19 | * Link that will be disabled if the target `href` is the same as the current 20 | * route's pathname. 21 | */ 22 | export const ActiveLink = React.forwardRef(function ActiveLink( 23 | { 24 | children, 25 | href, 26 | style, 27 | className, 28 | activeClassName, 29 | onClick, 30 | prefetch, 31 | compare = (a, b) => a === b, 32 | ...props 33 | }: ActiveLinkProps, 34 | ref 35 | ) { 36 | const pathname = usePathname() 37 | const [disabled, setDisabled] = React.useState(false) 38 | 39 | React.useEffect(() => { 40 | const linkPathname = new URL(href as string, location.href).pathname 41 | 42 | setDisabled(compare(linkPathname, pathname)) 43 | }, [pathname, href, compare]) 44 | 45 | const styleOverride = React.useMemo( 46 | () => 47 | disabled 48 | ? { 49 | ...style, 50 | pointerEvents: 'none' 51 | } 52 | : style ?? {}, 53 | [disabled, style] 54 | ) 55 | 56 | const onClickOverride = React.useCallback( 57 | (event: any): void => { 58 | if (disabled) { 59 | event.preventDefault() 60 | return 61 | } 62 | 63 | if (onClick) { 64 | onClick(event) 65 | return 66 | } 67 | }, 68 | [disabled, onClick] 69 | ) 70 | 71 | return ( 72 | 81 | {children} 82 | 83 | ) 84 | }) 85 | -------------------------------------------------------------------------------- /components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | 4 | import styles from './styles.module.css' 5 | 6 | export const Button: React.FC< 7 | { 8 | className?: string 9 | buttonClassName?: string 10 | children: React.ReactNode 11 | isLoading?: boolean 12 | ref?: any 13 | } & React.AnchorHTMLAttributes 14 | > = React.forwardRef(function Button( 15 | { className, buttonClassName, children, style, isLoading, ...buttonProps }, 16 | ref 17 | ) { 18 | return ( 19 |
24 | 25 |
{children}
26 |
27 |
28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /components/Button/styles.module.css: -------------------------------------------------------------------------------- 1 | .buttonWrapper { 2 | position: relative; 3 | } 4 | 5 | .button { 6 | position: relative; 7 | cursor: pointer; 8 | 9 | background: var(--fg-color); 10 | color: var(--bg-color); 11 | 12 | border: 1px solid transparent; 13 | box-shadow: 0 4px 4px 0 #00000010; 14 | transition-property: color, background-color, box-shadow; 15 | transition-duration: 0.15s; 16 | transition-timing-function: ease; 17 | padding: 12px 24px; 18 | line-height: 1.5em; 19 | border-radius: 5px; 20 | max-width: 100%; 21 | font-weight: 400; 22 | font-size: 1rem; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | user-select: none; 27 | outline: none; 28 | } 29 | 30 | .buttonContent { 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | overflow: hidden; 34 | display: inline-block; 35 | } 36 | 37 | .button:hover, 38 | .button:focus { 39 | border-color: var(--fg-color); 40 | background-color: var(--bg-color); 41 | color: var(--fg-color); 42 | } 43 | 44 | .button:active { 45 | background-color: var(--bg-color-1); 46 | } 47 | 48 | .buttonWrapper:has(.button:disabled) { 49 | opacity: 0.3; 50 | cursor: not-allowed; 51 | } 52 | 53 | .buttonWrapper:has(.button:disabled) * { 54 | pointer-events: none; 55 | } 56 | -------------------------------------------------------------------------------- /components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import cs from 'clsx' 5 | import { useGlitch } from 'react-powerglitch' 6 | 7 | import * as config from '@/lib/config' 8 | import { Discord, GitHub, Twitter } from '@/icons/index' 9 | 10 | import styles from './styles.module.css' 11 | 12 | export const Footer: React.FC<{ className?: string }> = ({ className }) => { 13 | const glitch = useGlitch({ playMode: 'hover' }) 14 | 15 | return ( 16 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /components/Footer/styles.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: space-between; 6 | align-items: center; 7 | gap: 1rem; 8 | } 9 | 10 | .settings, 11 | .copyright, 12 | .social { 13 | flex: 1; 14 | } 15 | 16 | .settings { 17 | justify-content: center; 18 | } 19 | 20 | .social { 21 | justify-content: flex-end; 22 | } 23 | 24 | .copyright { 25 | font-size: 80%; 26 | padding: 0.5rem 0; 27 | } 28 | 29 | .settings, 30 | .social { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | gap: 1rem; 35 | user-select: none; 36 | } 37 | 38 | .action { 39 | cursor: pointer; 40 | width: 2rem; 41 | height: 2rem; 42 | display: inline-flex; 43 | padding: 0.25rem; 44 | transition: color 250ms ease-out; 45 | } 46 | 47 | .footer .darkModeToggle { 48 | width: 1.75em; 49 | height: 1.75em; 50 | } 51 | 52 | .action:hover { 53 | transition: color 50ms ease-out; 54 | } 55 | 56 | .twitter:hover { 57 | color: #2795e9; 58 | } 59 | 60 | .github:hover { 61 | color: #c9510c; 62 | } 63 | 64 | .discord:hover { 65 | color: #5766f2; 66 | } 67 | 68 | @media only screen and (max-width: 500px) { 69 | .footer { 70 | flex-direction: column; 71 | } 72 | 73 | .footer .settings { 74 | justify-content: center; 75 | order: 1; 76 | } 77 | 78 | .footer .social { 79 | justify-content: center; 80 | order: 2; 81 | } 82 | 83 | .footer .copyright { 84 | justify-content: center; 85 | order: 3; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | import Link from 'next/link' 4 | import { useGlitch } from 'react-powerglitch' 5 | 6 | import * as config from '@/lib/config' 7 | import { ActiveLink } from '@/components/ActiveLink/ActiveLink' 8 | import { Discord, GitHub, Twitter } from '@/icons/index' 9 | 10 | import { Logo } from './Logo' 11 | import styles from './styles.module.css' 12 | 13 | export const Header: React.FC<{ className?: string }> = ({ className }) => { 14 | const glitch = useGlitch({ 15 | playMode: 'hover' 16 | }) 17 | 18 | return ( 19 |
20 |
21 | 22 | 23 | 24 | 25 |
26 | 32 | About 33 | 34 | 35 | 42 | 43 | 44 | 45 | 52 | 53 | 54 | 55 | 62 | 63 | 64 |
65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /components/Header/Logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import cs from 'clsx' 5 | import Image from 'next/image' 6 | import { useGlitch } from 'react-powerglitch' 7 | 8 | import LogoDark from '@/public/logo-dark.png' 9 | 10 | import styles from './styles.module.css' 11 | 12 | export const Logo: React.FC<{ className?: string }> = ({ className }) => { 13 | const glitch = useGlitch({ 14 | playMode: 'hover' 15 | }) 16 | 17 | return ( 18 | Logo 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/Header/styles.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | z-index: 200; 6 | 7 | width: 100%; 8 | max-width: 100vw; 9 | overflow: hidden; 10 | height: 72px; 11 | min-height: 72px; 12 | padding: 12px 0; 13 | 14 | background: hsla(0, 0%, 100%, 0.8); 15 | backdrop-filter: saturate(180%) blur(16px); 16 | } 17 | 18 | :global(.dark) .header { 19 | background: transparent; 20 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1); 21 | backdrop-filter: saturate(180%) blur(8px); 22 | } 23 | 24 | .navHeader { 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: space-between; 28 | align-items: center; 29 | gap: var(--gap-w-1); 30 | 31 | max-width: var(--max-width); 32 | height: 100%; 33 | margin: 0 auto; 34 | } 35 | 36 | .rhs { 37 | display: flex; 38 | flex-direction: row; 39 | justify-content: flex-end; 40 | align-items: center; 41 | height: 100%; 42 | gap: var(--gap-w-1); 43 | } 44 | 45 | .social { 46 | cursor: pointer; 47 | width: 2rem; 48 | height: 2rem; 49 | display: inline-flex; 50 | padding: 0.25rem; 51 | transition: color 250ms ease-out; 52 | } 53 | 54 | .social:hover { 55 | transition: color 50ms ease-out; 56 | } 57 | 58 | .action { 59 | display: inline-flex; 60 | align-items: center; 61 | padding: 12px; 62 | line-height: 1; 63 | font-size: 14px; 64 | border-radius: 3px; 65 | white-space: nowrap; 66 | text-overflow: ellipsis; 67 | background-color: transparent; 68 | cursor: pointer; 69 | outline: none; 70 | transition: 71 | backgrond-color 200ms ease-in, 72 | color 200ms ease-in; 73 | } 74 | 75 | .icon { 76 | box-sizing: border-box; 77 | padding: 8px !important; 78 | width: calc(1rem + 24px); 79 | height: calc(1rem + 24px); 80 | } 81 | 82 | .icon > svg { 83 | width: 100%; 84 | height: 100%; 85 | } 86 | 87 | .action:not(.active):hover, 88 | .action:not(.active):focus { 89 | transition: 90 | backgrond-color 50ms ease-in, 91 | color 50ms ease-in; 92 | background-color: var(--bg-color-0); 93 | } 94 | 95 | .action:not(.active):active { 96 | background-color: var(--bg-color-1); 97 | } 98 | 99 | .action.active { 100 | cursor: default; 101 | } 102 | 103 | .twitter:hover { 104 | color: #2795e9; 105 | } 106 | 107 | .github:hover { 108 | color: #c9510c; 109 | } 110 | 111 | .discord:hover { 112 | color: #5766f2; 113 | } 114 | 115 | .logo { 116 | display: block; 117 | width: auto; 118 | max-width: 100%; 119 | height: 48px; 120 | object-fit: contain; 121 | } 122 | 123 | /* Workaround for Firefox not supporting backdrop-filter yet */ 124 | /* @-moz-document url-prefix() { 125 | :global(.dark) .header { 126 | background: hsla(203, 8%, 20%, 0.8); 127 | } 128 | } */ 129 | -------------------------------------------------------------------------------- /components/HeroButton/HeroButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | 4 | import styles from './styles.module.css' 5 | 6 | export type HeroButtonVariant = 'orange' | 'blue' | 'purple' 7 | 8 | export const HeroButton: React.FC< 9 | { 10 | variant?: HeroButtonVariant 11 | className?: string 12 | buttonClassName?: string 13 | children: React.ReactNode 14 | } & React.AnchorHTMLAttributes 15 | > = ({ 16 | variant = 'purple', 17 | className, 18 | buttonClassName, 19 | children, 20 | style, 21 | ...buttonProps 22 | }) => { 23 | return ( 24 |
25 | {variant === 'blue' && ( 26 | 27 | )} 28 | 29 | {variant === 'purple' && ( 30 | 31 | )} 32 | 33 | {variant === 'orange' && ( 34 | 35 | )} 36 | 37 | 38 |
{children}
39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/HeroButton/styles.module.css: -------------------------------------------------------------------------------- 1 | .heroButtonWrapper { 2 | position: relative; 3 | } 4 | 5 | .heroButtonBg1 { 6 | --start-color: #00dfd8; 7 | --end-color: #007cf0; 8 | /* animation: heroBgAnimation1 8s infinite; */ 9 | } 10 | 11 | .heroButtonBg2 { 12 | --start-color: #ff0080; 13 | --end-color: #7928ca; 14 | /* animation: heroBgAnimation2 8s infinite; */ 15 | } 16 | 17 | .heroButtonBg3 { 18 | --start-color: #ff4d4d; 19 | --end-color: #f9cb28; 20 | /* animation: heroBgAnimation3 8s infinite; */ 21 | } 22 | 23 | .heroButtonBg { 24 | position: absolute; 25 | width: 100%; 26 | height: 100%; 27 | background-image: linear-gradient( 28 | 165deg, 29 | var(--start-color), 30 | var(--end-color) 31 | ); 32 | border-radius: 5px; 33 | z-index: -2; 34 | } 35 | 36 | .heroButtonBg::before { 37 | position: absolute; 38 | content: ''; 39 | top: 0; 40 | left: 0; 41 | right: 0; 42 | bottom: 0; 43 | z-index: -1; 44 | border: 12px solid transparent; 45 | filter: blur(24px); 46 | /* animation: pulse 2s ease-in-out infinite alternate; */ 47 | background-image: linear-gradient( 48 | 165deg, 49 | var(--start-color), 50 | var(--end-color) 51 | ); 52 | } 53 | 54 | .heroButton { 55 | position: relative; 56 | cursor: pointer; 57 | 58 | background-color: var(--bg-color); 59 | background-clip: padding-box; 60 | border: 1px solid transparent; 61 | box-shadow: 0 4px 4px 0 #00000010; 62 | color: var(--fg-color); 63 | transition-property: color, background-color, box-shadow; 64 | transition-duration: 0.15s; 65 | transition-timing-function: ease; 66 | padding: 12px 24px; 67 | line-height: 1.5em; 68 | border-radius: 5px; 69 | max-width: 100%; 70 | font-weight: 400; 71 | font-size: 1rem; 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | user-select: none; 76 | outline: none; 77 | 78 | --lighten-color: hsla(0, 0%, 100%, 0.8); 79 | background-image: linear-gradient( 80 | to right, 81 | var(--lighten-color), 82 | var(--lighten-color) 83 | ); 84 | } 85 | 86 | :global(.dark) .heroButton { 87 | --lighten-color: rgba(0, 0, 0, 0.75); 88 | } 89 | 90 | .heroButtonContent { 91 | text-overflow: ellipsis; 92 | white-space: nowrap; 93 | overflow: hidden; 94 | display: inline-block; 95 | } 96 | 97 | .heroButton:hover { 98 | --lighten-color: transparent; 99 | background-color: transparent; 100 | color: var(--bg-color); 101 | } 102 | 103 | .heroButton:focus:not(:active):not(:hover) { 104 | border-color: var(--fg-color); 105 | } 106 | 107 | .heroButton:active { 108 | --lighten-color: hsla(0, 0%, 100%, 0.5); 109 | } 110 | 111 | .heroButtonWrapper:has(.heroButton:disabled) { 112 | opacity: 0.3; 113 | cursor: not-allowed; 114 | } 115 | 116 | .heroButtonWrapper:has(.heroButton:disabled) * { 117 | pointer-events: none; 118 | } 119 | 120 | /* @keyframes heroBgAnimation1 { 121 | 0%, 122 | 16.667%, 123 | to { 124 | opacity: 1; 125 | } 126 | 33%, 127 | 83.333% { 128 | opacity: 0; 129 | } 130 | } 131 | 132 | @keyframes heroBgAnimation2 { 133 | 0%, 134 | 16.667%, 135 | 66.667%, 136 | to { 137 | opacity: 0; 138 | } 139 | 33.333%, 140 | 50% { 141 | opacity: 1; 142 | } 143 | } 144 | 145 | @keyframes heroBgAnimation3 { 146 | 0%, 147 | 50%, 148 | to { 149 | opacity: 0; 150 | } 151 | 66.667%, 152 | 83.333% { 153 | opacity: 1; 154 | } 155 | } */ 156 | 157 | /* @keyframes pulse { 158 | from { 159 | filter: blur(8px); 160 | } 161 | 162 | to { 163 | filter: blur(32px); 164 | } 165 | } */ 166 | -------------------------------------------------------------------------------- /components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | import { Inter } from 'next/font/google' 4 | 5 | import { Footer } from '@/components/Footer/Footer' 6 | import { Header } from '@/components/Header/Header' 7 | 8 | import { RootLayoutProviders } from './providers' 9 | import styles from './styles.module.css' 10 | 11 | const inter = Inter({ subsets: ['latin'] }) 12 | 13 | export function Layout({ children }: { children: React.ReactNode }) { 14 | return ( 15 |
16 | 17 |
18 | 19 |
{children}
20 | 21 |
22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/Layout/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { MotionConfig } from 'framer-motion' 5 | 6 | export function RootLayoutProviders({ 7 | children 8 | }: { 9 | children: React.ReactNode 10 | }) { 11 | return {children} 12 | } 13 | -------------------------------------------------------------------------------- /components/Layout/styles.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | flex: 1; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: space-around; 6 | align-items: center; 7 | margin: 0 auto; 8 | max-width: var(--max-width); 9 | min-height: 100vh; 10 | padding: 0; 11 | } 12 | 13 | .header { 14 | padding: 12px 12px 0; 15 | } 16 | 17 | .footer { 18 | padding: 0 12px 12px; 19 | } 20 | 21 | .main { 22 | flex: 1; 23 | width: 100%; 24 | max-width: var(--max-body-width); 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | margin-bottom: var(--gap-h); 29 | padding: 0 12px; 30 | } 31 | 32 | @media (max-width: 800px) { 33 | .header { 34 | padding: 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/Markdown/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | 4 | import styles from './styles.module.css' 5 | 6 | /** 7 | * `content` is assumed to have already been transformed into HTML via remark/rehype. 8 | */ 9 | export const Markdown: React.FC<{ className?: string; content: string }> = ({ 10 | className, 11 | content 12 | }) => { 13 | return ( 14 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/Markdown/styles.module.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | max-width: 830px; 3 | line-height: 1.6; 4 | } 5 | 6 | .markdown > *:first-child { 7 | margin-top: 0 !important; 8 | } 9 | 10 | .markdown img { 11 | display: inline-block; 12 | max-width: 100%; 13 | margin: 0; 14 | } 15 | 16 | /* TODO: this is hacky */ 17 | .markdown img[height] { 18 | height: 65px; 19 | } 20 | 21 | .markdown a:not(:has(img)) { 22 | /* color: #0969da; 23 | text-decoration: none;*/ 24 | display: inline-block; 25 | text-decoration: none; 26 | font-weight: inherit; 27 | line-height: 1.3; 28 | position: relative; 29 | transition: unset; 30 | opacity: 1; 31 | color: unset; 32 | border-color: var(--fg-color-2); 33 | border-bottom-width: 0.135rem; 34 | background: transparent; 35 | background-origin: border-box; 36 | background-repeat: no-repeat; 37 | background-position: 50% 100%; 38 | background-size: 0 0.135rem; 39 | } 40 | 41 | /* 42 | .markdown a { 43 | color: #0969da; 44 | text-decoration: none; 45 | 46 | :global(.dark) .markdown a { 47 | color: #3291ff; 48 | } 49 | 50 | .markdown a:hover { 51 | text-decoration: underline; 52 | } */ 53 | 54 | .markdown a:hover:not(:has(img)), 55 | .markdown a:focus:not(:has(img)) { 56 | color: var(--tw-prose-links); 57 | text-decoration: none; 58 | border-bottom-color: transparent; 59 | 60 | background-image: linear-gradient(90.68deg, #b439df 0.26%, #e5337e 102.37%); 61 | background-repeat: no-repeat; 62 | background-position: 0 100%; 63 | background-size: 100% 0.135rem; 64 | 65 | transition-property: background-position, background-size; 66 | transition-duration: 300ms; 67 | } 68 | 69 | .markdown p { 70 | margin-top: 0; 71 | margin-bottom: 1em; 72 | } 73 | 74 | .markdown li a { 75 | margin: 0; 76 | } 77 | 78 | .markdown h1, 79 | .markdown h2, 80 | .markdown h3, 81 | .markdown h4, 82 | .markdown h5, 83 | .markdown h6 { 84 | margin-top: 2em; 85 | margin-bottom: 0.5em; 86 | font-weight: 600; 87 | line-height: 1.25; 88 | } 89 | 90 | .markdown h1 { 91 | font-size: 2em; 92 | border-bottom: 1px solid var(--var-bg-2); 93 | } 94 | 95 | .markdown h2 { 96 | font-size: 1.5em; 97 | border-bottom: 1px solid var(--var-bg-2); 98 | } 99 | 100 | .markdown h3 { 101 | font-size: 1.25em; 102 | } 103 | 104 | .markdown h4 { 105 | font-size: 1em; 106 | } 107 | 108 | .markdown h5 { 109 | font-size: 1em; 110 | font-weight: 700; 111 | } 112 | 113 | .markdown h6 { 114 | font-size: 1em; 115 | font-weight: 500; 116 | } 117 | 118 | @media (max-width: 800px) { 119 | .markdown img { 120 | display: block; 121 | text-align: center; 122 | margin: 0 auto; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /components/MetaballVisualization/Body.ts: -------------------------------------------------------------------------------- 1 | // scaling factor that affects the mass of the bodies, effectively scaling the 2 | // overall gravity strength of the system 3 | const GRAVITY_SPEED = 900 4 | const MAX_GRAVITY_SPEED = 1000 5 | 6 | export class Body { 7 | // position 8 | x: number 9 | y: number 10 | 11 | // velocity vector 12 | dx: number 13 | dy: number 14 | 15 | // affects this body's gravitational pull w.r.t. other bodies 16 | mass: number 17 | r: number 18 | 19 | visible: boolean 20 | 21 | constructor({ 22 | x, 23 | y, 24 | dx = 0, 25 | dy = 0, 26 | r, 27 | visible = true 28 | }: { 29 | x: number 30 | y: number 31 | r: number 32 | dx?: number 33 | dy?: number 34 | visible?: boolean 35 | }) { 36 | this.x = x 37 | this.y = y 38 | 39 | this.dx = dx 40 | this.dy = dy 41 | 42 | this.r = r 43 | this.mass = (2 * this.r) / (MAX_GRAVITY_SPEED - GRAVITY_SPEED) 44 | this.visible = visible 45 | } 46 | 47 | addAcceleration(accelX: number, accelY: number) { 48 | this.dx += accelX 49 | this.dy += accelY 50 | } 51 | 52 | update() { 53 | if (!this.visible) { 54 | return 55 | } 56 | 57 | this.x += this.dx 58 | this.y += this.dy 59 | } 60 | 61 | // returns whether or not this body intersects the given body (currently unused) 62 | intersects(body: Body): boolean { 63 | const radius = this.r 64 | const rad = body.r 65 | const xDif = body.x - this.x 66 | const yDif = body.y - this.y 67 | const dist = Math.sqrt(xDif * xDif + yDif * yDif) 68 | 69 | // reject if dist btwn circles is greater than their radii combined 70 | if (dist > radius + rad) { 71 | return false 72 | } 73 | 74 | // reject if one circle is inside of the other 75 | return dist >= Math.abs(rad - radius) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /components/MetaballVisualization/MetaballVisualization.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useMeasure } from 'react-use' 3 | 4 | import { Metaballs } from '@/store/metaballs' 5 | 6 | import { MetaballViz } from './MetaballViz' 7 | import styles from './styles.module.css' 8 | 9 | export const MetaballVisualization: React.FC = () => { 10 | const { metaballVizRef } = Metaballs.useContainer() 11 | const canvasRef = React.useRef(null) 12 | 13 | const defaultWidth = typeof window !== 'undefined' ? window.innerWidth : 1280 14 | const defaultHeight = typeof window !== 'undefined' ? window.innerHeight : 720 15 | const [measureRef, { width = defaultWidth, height = defaultHeight }] = 16 | useMeasure() 17 | 18 | React.useEffect(() => { 19 | if (!canvasRef.current) return 20 | 21 | const metaballViz = new MetaballViz({ 22 | canvas: canvasRef.current 23 | }) 24 | metaballVizRef.current = metaballViz 25 | metaballViz.animate() 26 | 27 | return () => metaballViz.destroy() 28 | }, [canvasRef, metaballVizRef]) 29 | 30 | React.useEffect(() => { 31 | if (!metaballVizRef?.current) return 32 | metaballVizRef.current.onResize() 33 | }, [width, height, metaballVizRef]) 34 | 35 | const onMouseMove = React.useCallback( 36 | (event: any) => { 37 | if (!metaballVizRef?.current) return 38 | metaballVizRef.current.onMouseMove(event) 39 | canvasRef.current?.focus() 40 | }, 41 | [metaballVizRef, canvasRef] 42 | ) 43 | 44 | return ( 45 |
46 | 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /components/MetaballVisualization/MetaballViz.ts: -------------------------------------------------------------------------------- 1 | import random from 'random' 2 | import { 3 | Mesh, 4 | OrthographicCamera, 5 | PlaneGeometry, 6 | Scene, 7 | ShaderMaterial, 8 | Vector2, 9 | WebGLRenderer 10 | } from 'three' 11 | 12 | import { Body } from './Body' 13 | 14 | // https://www.shadertoy.com/view/XssSzN 15 | // https://blog.hyuntak.com/metaball 16 | 17 | export class MetaballViz { 18 | _canvas: HTMLCanvasElement 19 | _renderer: WebGLRenderer 20 | _scene: Scene 21 | _camera: OrthographicCamera 22 | _material: ShaderMaterial 23 | 24 | _metaballs: Body[] 25 | _metaballsData: Float32Array 26 | _numMetaballs: number 27 | 28 | _rafHandle?: number | null 29 | // _mouse?: Vector2 30 | 31 | constructor({ 32 | canvas, 33 | numMetaballs = 50 34 | }: { 35 | canvas: HTMLCanvasElement 36 | numMetaballs?: number 37 | }) { 38 | this._canvas = canvas 39 | this._numMetaballs = numMetaballs 40 | 41 | const { width, height } = this._canvas 42 | 43 | this._renderer = new WebGLRenderer({ 44 | antialias: true, 45 | alpha: true, 46 | canvas: this._canvas 47 | }) 48 | this._renderer.setSize(width, height) 49 | this._renderer.setClearColor(0x121212) 50 | // this._renderer.setPixelRatio(window.devicePixelRatio) 51 | 52 | this._camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1) 53 | this._scene = new Scene() 54 | const geometry = new PlaneGeometry(2, 2) 55 | 56 | const vertexShader = ` 57 | void main() { 58 | gl_Position = vec4(position, 1.0); 59 | } 60 | ` 61 | 62 | // const fragmentShader = ` 63 | // uniform vec2 screen; 64 | // // uniform vec2 mouse; 65 | // uniform vec3 metaballs[${numMetaballs}]; 66 | 67 | // void main(){ 68 | // float x = gl_FragCoord.x; 69 | // float y = gl_FragCoord.y; 70 | 71 | // float sum = 0.0; 72 | // for (int i = 0; i < ${numMetaballs}; i++) { 73 | // vec3 metaball = metaballs[i]; 74 | // float dx = metaball.x - x; 75 | // float dy = metaball.y - y; 76 | // float radius = metaball.z; 77 | 78 | // sum += (radius * radius) / (dx * dx + dy * dy); 79 | // // sum += pow((radius * radius) / (dx * dx + dy * dy), 0.8); 80 | // } 81 | 82 | // // if (mouse.x > 0.0 && mouse.y > 0.0) { 83 | // // float dx = mouse.x - x; 84 | // // float dy = (screen.y - mouse.y) - y; 85 | // // float radius = 50.0; 86 | // // sum -= (radius * radius) / (dx * dx + dy * dy); 87 | // // } 88 | 89 | // if (sum >= 0.99) { 90 | // gl_FragColor = vec4(mix(vec3(x / screen.x, y / screen.y, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0); 91 | // return; 92 | // } 93 | 94 | // discard; 95 | // } 96 | // ` 97 | 98 | const fragmentShader = ` 99 | uniform vec2 screen; 100 | uniform vec3 metaballs[${numMetaballs}]; 101 | 102 | void main(){ 103 | float x = gl_FragCoord.x; 104 | float y = gl_FragCoord.y; 105 | 106 | float sum = 0.0; 107 | for (int i = 0; i < ${numMetaballs}; i++) { 108 | vec3 metaball = metaballs[i]; 109 | float dx = metaball.x - x; 110 | float dy = metaball.y - y; 111 | float radius = metaball.z; 112 | 113 | sum += (radius * radius) / (dx * dx + dy * dy); 114 | } 115 | 116 | if (sum >= 0.99) { 117 | gl_FragColor = vec4(mix(vec3(x / screen.x, y / screen.y, 1.0), vec3(0, 0, 0), max(0.0, 1.0 - (sum - 0.99) * 100.0)), 1.0); 118 | } else { 119 | gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); 120 | } 121 | } 122 | 123 | ` 124 | 125 | this._metaballs = [] 126 | this._metaballsData = new Float32Array(3 * this._numMetaballs) 127 | 128 | this._resetMetaballs() 129 | this.update() 130 | 131 | this._material = new ShaderMaterial({ 132 | uniforms: { 133 | screen: { 134 | value: new Vector2(width, height) 135 | }, 136 | // mouse: { 137 | // value: new Vector2(0, 0) 138 | // }, 139 | metaballs: { 140 | value: this._metaballsData 141 | } 142 | }, 143 | vertexShader, 144 | fragmentShader 145 | // transparent: true 146 | }) 147 | 148 | const mesh = new Mesh(geometry, this._material) 149 | this._scene.add(mesh) 150 | } 151 | 152 | _resetMetaballs() { 153 | const { width, height } = this._canvas 154 | 155 | const x0 = width / 2 156 | const y0 = height / 2 157 | 158 | this._metaballs = [] 159 | this._metaballs.push( 160 | new Body({ 161 | x: x0, 162 | y: y0, 163 | r: 500, 164 | dx: 0, 165 | dy: 0, 166 | visible: false 167 | }) 168 | ) 169 | 170 | const r0 = Math.sqrt(width * width + height * height) / 10 171 | 172 | for (let i = 1; i < this._numMetaballs + 1; i++) { 173 | const theta = ((i - 1) * 2.0 * Math.PI) / this._numMetaballs 174 | const x = x0 + r0 * Math.cos(theta) 175 | const y = y0 + r0 * Math.sin(theta) 176 | const dx = 0 177 | const dy = 0 178 | const r = random.float(10, 45) 179 | 180 | // const r = random.float(10, 70) * 0.75 181 | // const x = random.float(r, width - r) 182 | // const y = random.float(r, height - r) 183 | // const dx = random.float(-1.5, 1.5) 184 | // const dy = random.float(-1.5, 1.5) 185 | 186 | this._metaballs.push( 187 | new Body({ 188 | x, 189 | y, 190 | dx, 191 | dy, 192 | r 193 | }) 194 | ) 195 | } 196 | } 197 | 198 | destroy() { 199 | this.pause() 200 | } 201 | 202 | pause() { 203 | if (this._rafHandle) { 204 | cancelAnimationFrame(this._rafHandle) 205 | this._rafHandle = null 206 | } 207 | } 208 | 209 | animate() { 210 | this.update() 211 | this.render() 212 | 213 | this._rafHandle = requestAnimationFrame(this.animate.bind(this)) 214 | } 215 | 216 | onResize = () => { 217 | const { width, height } = this._canvas 218 | 219 | this._resetMetaballs() 220 | 221 | this._renderer.setSize(width, height) 222 | this._material.uniforms.screen.value = new Vector2(width, height) 223 | } 224 | 225 | update() { 226 | const width = this._canvas.width 227 | const height = this._canvas.height 228 | 229 | // const maxInvDist = 0.0001 230 | const maxInvDist = 10.0 231 | 232 | // for each body in the system, enact a force on every other body in the system 233 | // running time O(num_particles ^ 2) 234 | for (let i = 0; i < this._numMetaballs + 1; i++) { 235 | const mi = this._metaballs[i] 236 | 237 | for (let j = i + 1; j < this._numMetaballs + 1; j++) { 238 | const mj = this._metaballs[j] 239 | let xDif = mj.x - mi.x 240 | let yDif = mj.y - mi.y 241 | const invDistSquared = Math.min( 242 | 1.0 / (xDif * xDif + yDif * yDif), 243 | maxInvDist 244 | ) 245 | if (xDif === 0 || yDif === 0) { 246 | continue 247 | } 248 | 249 | // force is inversely proportional to the distance squared 250 | xDif *= invDistSquared 251 | yDif *= invDistSquared 252 | 253 | mi.addAcceleration(xDif * mj.mass, yDif * mj.mass) 254 | mj.addAcceleration(-xDif * mi.mass, -yDif * mi.mass) 255 | } 256 | 257 | // if (this._mouse) { 258 | // const mj = this._mouse 259 | // let xDif = mj.x - mi.x 260 | // let yDif = height - mj.y - mi.y 261 | // const invDistSquared = 1.0 / (xDif * xDif + yDif * yDif) 262 | // if (xDif === 0 || yDif === 0) { 263 | // continue 264 | // } 265 | 266 | // xDif *= invDistSquared 267 | // yDif *= invDistSquared 268 | 269 | // mi.addAcceleration(-xDif * 20, -yDif * 20) 270 | // } 271 | } 272 | 273 | const maxV = 10 274 | // let maxDx = 0 275 | // let maxDy = 0 276 | 277 | for (let i = 1; i < this._numMetaballs + 1; i++) { 278 | const metaball = this._metaballs[i] 279 | metaball.update() 280 | 281 | if (metaball.x < metaball.r) { 282 | metaball.dx = Math.abs(metaball.dx) 283 | } 284 | if (metaball.x > width - metaball.r) { 285 | metaball.dx = -Math.abs(metaball.dx) 286 | } 287 | 288 | if (metaball.y < metaball.r) { 289 | metaball.dy = Math.abs(metaball.dy) 290 | } 291 | 292 | if (metaball.y > height - metaball.r) { 293 | metaball.dy = -Math.abs(metaball.dy) 294 | } 295 | 296 | if (metaball.dx > maxV) { 297 | metaball.dx = maxV 298 | } 299 | 300 | if (metaball.dx < -maxV) { 301 | metaball.dx = -maxV 302 | } 303 | 304 | if (metaball.dy > maxV) { 305 | metaball.dy = maxV 306 | } 307 | 308 | if (metaball.dy < -maxV) { 309 | metaball.dy = -maxV 310 | } 311 | 312 | // if (Math.abs(metaball.dx) > maxDx) { 313 | // maxDx = Math.abs(metaball.dx) 314 | // } 315 | 316 | // if (Math.abs(metaball.dy) > maxDy) { 317 | // maxDy = Math.abs(metaball.dy) 318 | // } 319 | 320 | const baseIndex = 3 * (i - 1) 321 | this._metaballsData[baseIndex + 0] = metaball.x 322 | this._metaballsData[baseIndex + 1] = metaball.y 323 | this._metaballsData[baseIndex + 2] = metaball.r 324 | } 325 | } 326 | 327 | onMouseMove = (event: any) => { 328 | // const { clientX, clientY } = this.getEventCoordinates(event) 329 | // this._mouse = new Vector2(clientX, clientY) 330 | // console.log({ clientX, clientY }) 331 | // this._material.uniforms.mouse.value = new Vector2(clientX, clientY) 332 | } 333 | 334 | getEventCoordinates(event: any) { 335 | let clientX = 0 336 | let clientY = 0 337 | 338 | if (event.type === 'touchmove' || event.type === 'touchstart') { 339 | clientX = event.touches[0].clientX 340 | clientY = event.touches[0].clientY 341 | } else { 342 | clientX = event.clientX 343 | clientY = event.clientY 344 | } 345 | 346 | return { clientX, clientY } 347 | } 348 | 349 | render() { 350 | this._material.uniforms.metaballs.value = this._metaballsData 351 | this._material.uniformsNeedUpdate = true 352 | 353 | this._renderer.render(this._scene, this._camera) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /components/MetaballVisualization/styles.module.css: -------------------------------------------------------------------------------- 1 | .metaballVisualization { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100vh; 7 | min-height: 480px; 8 | max-height: 4096px; 9 | } 10 | 11 | .metaballVisualization canvas { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | user-select: none; 16 | outline: none; 17 | width: 100%; 18 | height: 100%; 19 | max-width: 100vw; 20 | max-height: 100vh; 21 | } 22 | -------------------------------------------------------------------------------- /components/PageHead/PageHead.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Head from 'next/head' 3 | 4 | import * as config from '@/lib/config' 5 | 6 | export const PageHead: React.FC<{ 7 | title?: string 8 | description?: string 9 | imagePathname?: string 10 | pathname?: string 11 | }> = ({ 12 | title = config.title, 13 | description = config.description, 14 | imagePathname, 15 | pathname 16 | }) => { 17 | const url = pathname ? `${config.url}${pathname}` : config.url 18 | const imageUrl = imagePathname 19 | ? `${config.url}${imagePathname}` 20 | : config.socialImageUrl 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {description && ( 33 | <> 34 | 35 | 36 | 37 | 38 | )} 39 | 40 | {imageUrl ? ( 41 | <> 42 | 43 | 44 | 45 | 46 | ) : ( 47 | 48 | )} 49 | 50 | {url && ( 51 | <> 52 | 53 | 54 | 55 | 56 | )} 57 | 58 | 59 | 60 | {title} 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /components/WebGLSupportChecker/WebGLSupportChecker.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | interface WebGLSupportCheckerProps { 4 | children: ReactNode 5 | fallback: ReactNode 6 | } 7 | 8 | const WebGLSupportChecker: React.FC = ({ 9 | children, 10 | fallback 11 | }) => { 12 | const isWebGLSupported = () => { 13 | try { 14 | const canvas = document.createElement('canvas') 15 | return !!( 16 | window.WebGLRenderingContext && 17 | (canvas.getContext('webgl') || canvas.getContext('experimental-webgl')) 18 | ) 19 | } catch (e) { 20 | return false 21 | } 22 | } 23 | 24 | return isWebGLSupported() ? <>{children} : <>{fallback} 25 | } 26 | 27 | export default WebGLSupportChecker 28 | -------------------------------------------------------------------------------- /icons/Discord.tsx: -------------------------------------------------------------------------------- 1 | export const Discord = ({ className }: { className?: string }) => ( 2 | // 12 | // 13 | // 14 | 15 | 23 | 24 | 28 | 29 | 30 | ) 31 | -------------------------------------------------------------------------------- /icons/GitHub.tsx: -------------------------------------------------------------------------------- 1 | export const GitHub = ({ className }: { className?: string }) => ( 2 | 3 | 4 | 5 | ) 6 | -------------------------------------------------------------------------------- /icons/Twitter.tsx: -------------------------------------------------------------------------------- 1 | export const Twitter = ({ className }: { className?: string }) => ( 2 | 3 | 4 | 5 | ) 6 | -------------------------------------------------------------------------------- /icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GitHub' 2 | export * from './Discord' 3 | export * from './Twitter' 4 | -------------------------------------------------------------------------------- /lib/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { githubRepoUrl, isSafari, isServer } from './config' 2 | 3 | const detail = `This webapp is open source: ${githubRepoUrl}` 4 | const banner = ` 5 | 6 | ████████╗██████╗ █████╗ ███╗ ██╗███████╗██╗████████╗██╗██╗ ██╗███████╗ ██████╗ ███████╗ 7 | ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║╚══██╔══╝██║██║ ██║██╔════╝ ██╔══██╗██╔════╝ 8 | ██║ ██████╔╝███████║██╔██╗ ██║███████╗██║ ██║ ██║██║ ██║█████╗ ██████╔╝███████╗ 9 | ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗╚════██║ 10 | ██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ███████╗ ██████╔╝███████║ 11 | ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚══════╝ 12 | 13 | ${detail} 14 | ` 15 | 16 | export async function bootstrap() { 17 | if (isServer) return 18 | 19 | if (isSafari) { 20 | console.log(detail) 21 | } else { 22 | console.log(banner) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | export const environment = process.env.NODE_ENV || 'development' 2 | export const isDev = environment === 'development' 3 | export const isServer = typeof window === 'undefined' 4 | export const isSafari = 5 | !isServer && /^((?!chrome|android).)*safari/i.test(navigator.userAgent) 6 | 7 | export const title = 'ChatGPT Hackers' 8 | export const description = 9 | 'Join thousands of other developers, researchers, and AI enthusiasts who are building at the cutting edge of AI!' 10 | export const domain = 'www.chatgpthackers.dev' 11 | 12 | export const author = 'Travis Fischer' 13 | export const twitter = 'transitive_bs' 14 | export const twitterUrl = `https://twitter.com/${twitter}` 15 | export const discordUrl = 'https://discord.gg/v9gERj825w' 16 | export const githubRepoUrl = 17 | 'https://github.com/transitive-bullshit/chatgpt-hackers' 18 | export const githubSponsorsUrl = 19 | 'https://github.com/sponsors/transitive-bullshit' 20 | export const copyright = `Copyright 2023 ${author}` 21 | export const madeWithLove = 'Made with ❤️ in Brooklyn, NY' 22 | 23 | export const port = process.env.PORT || '3000' 24 | export const prodUrl = `https://${domain}` 25 | export const url = isDev ? `http://localhost:${port}` : prodUrl 26 | 27 | export const apiBaseUrl = 28 | isDev || !process.env.VERCEL_URL ? url : `https://${process.env.VERCEL_URL}` 29 | 30 | // these must all be absolute urls 31 | export const socialImageUrl = `${url}/social.jpg` 32 | -------------------------------------------------------------------------------- /lib/markdown-to-html.ts: -------------------------------------------------------------------------------- 1 | import rehypeFormat from 'rehype-format' 2 | import rehypeRaw from 'rehype-raw' 3 | import rehypeStringify from 'rehype-stringify' 4 | import remarkGfm from 'remark-gfm' 5 | import remarkParse from 'remark-parse' 6 | import remarkRehype from 'remark-rehype' 7 | import { unified } from 'unified' 8 | 9 | const processor = unified() 10 | .use(remarkParse) 11 | .use(remarkGfm) 12 | .use(remarkRehype, { 13 | allowDangerousHtml: true 14 | }) 15 | .use(rehypeRaw) 16 | .use(rehypeFormat) 17 | .use(rehypeStringify) 18 | 19 | export async function markdownToHtml(markdown: string) { 20 | const result = await processor.process(markdown) 21 | return result.toString() 22 | } 23 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Travis Fischer 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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true 4 | } 5 | 6 | export default nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-hackers", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "Website for the ChatGPT Hackers community.", 6 | "author": "Travis Fischer ", 7 | "repository": "transitive-bullshit/chatgpt-hackers", 8 | "homepage": "https://www.chatgpthackers.dev", 9 | "license": "MIT", 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "scripts": { 14 | "dev": "next dev", 15 | "build": "next build", 16 | "start": "next start", 17 | "prepare": "husky install", 18 | "pre-commit": "lint-staged", 19 | "test": "run-p test:*", 20 | "test:lint": "next lint", 21 | "test:prettier": "prettier '**/*.{js,jsx,ts,tsx}' --check" 22 | }, 23 | "dependencies": { 24 | "clsx": "^1.2.1", 25 | "framer-motion": "^10.10.0", 26 | "human-number": "^2.0.1", 27 | "next": "13.2.4", 28 | "raf": "^3.4.1", 29 | "random": "^4.1.0", 30 | "react": "18.2.0", 31 | "react-dom": "18.2.0", 32 | "react-powerglitch": "^1.0.0", 33 | "react-use": "^17.4.0", 34 | "rehype-format": "^4.0.1", 35 | "rehype-raw": "^6.1.1", 36 | "rehype-stringify": "^9.0.3", 37 | "remark-gfm": "^3.0.1", 38 | "remark-parse": "^10.0.1", 39 | "remark-rehype": "^10.1.0", 40 | "three": "^0.151.3", 41 | "unified": "^10.1.2", 42 | "unstated-next": "^1.1.0" 43 | }, 44 | "devDependencies": { 45 | "@tailwindcss/typography": "^0.5.9", 46 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 47 | "@types/node": "18.15.11", 48 | "@types/react": "18.0.33", 49 | "@types/react-dom": "18.0.11", 50 | "@types/three": "^0.150.1", 51 | "@vercel/analytics": "^0.1.11", 52 | "autoprefixer": "^10.4.14", 53 | "eslint": "8.37.0", 54 | "eslint-config-next": "13.2.4", 55 | "husky": "^8.0.3", 56 | "lint-staged": "^13.2.0", 57 | "npm-run-all": "^4.1.5", 58 | "postcss": "^8.4.21", 59 | "postcss-import": "^15.1.0", 60 | "prettier": "^2.8.7", 61 | "tailwindcss": "^3.3.1", 62 | "typescript": "5.0.3" 63 | }, 64 | "lint-staged": { 65 | "*.{ts,tsx}": [ 66 | "prettier --write" 67 | ] 68 | }, 69 | "keywords": [ 70 | "chatgpt", 71 | "openai", 72 | "ai", 73 | "hackers", 74 | "hacking", 75 | "gpt", 76 | "gpt-3", 77 | "gpt3", 78 | "gpt4", 79 | "gpt-4", 80 | "chatbot", 81 | "machine learning", 82 | "ml", 83 | "bot", 84 | "oss", 85 | "open source" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Analytics } from '@vercel/analytics/react' 3 | import type { AppProps } from 'next/app' 4 | import Head from 'next/head' 5 | 6 | import { bootstrap } from '@/lib/bootstrap' 7 | import { isServer } from '@/lib/config' 8 | import '@/styles/globals.css' 9 | 10 | if (!isServer) { 11 | bootstrap() 12 | } 13 | 14 | export default function App({ 15 | Component, 16 | pageProps: { session, ...pageProps } 17 | }: AppProps) { 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Head, Html, Main, NextScript } from 'next/document' 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /pages/about/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { InferGetStaticPropsType } from 'next' 3 | 4 | import * as config from '@/lib/config' 5 | import { Layout } from '@/components/Layout/Layout' 6 | import { Markdown } from '@/components/Markdown/Markdown' 7 | import { markdownToHtml } from '@/lib/markdown-to-html' 8 | 9 | import styles from './styles.module.css' 10 | 11 | const markdownContent = ` 12 | ## About 13 | 14 | When ChatGPT launched at the end of 2022, the world changed for some of us. Within a few days, we had built multiple open source ChatGPT API wrappers for [Python](https://github.com/acheong08/ChatGPT) and [Node.js](https://github.com/transitive-bullshit/chatgpt-api), and these projects quickly skyrocketed to the top of GitHub Trending. 15 | 16 | Since then, tens of thousands of developers have used our open source libraries to build amazing projects, bots, extensions, experiments, and products. We've honestly been blown away by the creativity and passion of the community, and so we decided to create a [Discord server](${config.discordUrl}) to help bring everyone together. 17 | 18 | Our community's grown rapidly over the past few months, and we now have **over 7000 members**. We're a group of developers, researchers, hackers, and AI enthusiasts who are all excited about building at the cutting edge of AI. Open source is also very much at the heart of what we do, and our community members are always building fun / crazy / useful OSS projects that push the boundaries of what's possible with the latest advances in AI. 19 | 20 | ## Admins 21 | 22 | - [Travis Fischer aka transitive-bullshit](https://twitter.com/transitive_bs) - [github](https://github.com/transitive-bullshit), [twitter](https://twitter.com/transitive_bs), [linkedin](https://www.linkedin.com/in/fisch2/) 23 | - [Antonio Cheong aka acheong08](https://twitter.com/GodlyIgnorance) - [github](https://github.com/acheong08), [twitter](https://twitter.com/GodlyIgnorance), [linkedin](https://www.linkedin.com/in/acheong08/) 24 | - [Joel Zhang aka waylaidwanderer](https://twitter.com/TheCodeOfJoel) - [github](https://github.com/waylaidwanderer), [twitter](https://twitter.com/TheCodeOfJoel), [linkedin](https://www.linkedin.com/in/joelczhang/) 25 | - [Rawand Ahmed Shaswar aka rawa](https://twitter.com/RawandShaswar) - [github](https://github.com/rawandahmad698), [twitter](https://twitter.com/RawandShaswar), [linkedin](https://www.linkedin.com/in/rawand-ahmed-shaswar-39a945215/) 26 | 27 | ## Selected OSS AI Projects 28 | 29 | Here's just a few of the amazing OSS projects that have been built by our community members. If you're a member of our community and you build something cool with AI, [create a PR](https://github.com/transitive-bullshit/chatgpt-hackers), and we'll add it to the list! 30 | 31 | ### JavaScript / TypeScript 32 | 33 | - [chatgpt](https://github.com/transitive-bullshit/chatgpt-api) [![](https://img.shields.io/github/stars/transitive-bullshit/chatgpt-api?style=social)](https://github.com/transitive-bullshit/chatgpt-api) 34 | - Node.js client for the official ChatGPT API 🔥 35 | - Also supports the unofficial API 36 | - [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api) [![](https://img.shields.io/github/stars/waylaidwanderer/node-chatgpt-api?style=social)](https://github.com/waylaidwanderer/node-chatgpt-api) 37 | - ChatGPT and Bing AI clients 38 | - Available as a Node.js module, REST API server, and CLI 39 | - [bing-chat](https://github.com/transitive-bullshit/bing-chat) [![](https://img.shields.io/github/stars/transitive-bullshit/bing-chat?style=social)](https://github.com/transitive-bullshit/bing-chat) 40 | - Node.js client for Bing's new AI-powered search 41 | 42 | ### Python 43 | 44 | - [revChatGPT](https://github.com/acheong08/ChatGPT) [![](https://img.shields.io/github/stars/acheong08/ChatGPT?style=social)](https://github.com/acheong08/ChatGPT) 45 | - Most widely used ChatGPT API wrapper for Python 46 | - Supports both the official and unofficial APIs 47 | - [PyChatGPT](https://github.com/rawandahmad698/PyChatGPT) [![](https://img.shields.io/github/stars/rawandahmad698/PyChatGPT?style=social)](https://github.com/rawandahmad698/PyChatGPT) 48 | - ️ Python client for the unofficial ChatGPT API with auto token regeneration, conversation tracking, proxy support and more ⚡ 49 | - [EdgeGPT](https://github.com/acheong08/EdgeGPT) [![](https://img.shields.io/github/stars/acheong08/EdgeGPT?style=social)](https://github.com/acheong08/EdgeGPT) 50 | - Reverse engineered API for Microsoft's Bing Chat 51 | 52 | ### Bots 53 | 54 | - [chatgpt-twitter-bot](https://github.com/transitive-bullshit/chatgpt-twitter-bot) [![](https://img.shields.io/github/stars/transitive-bullshit/chatgpt-twitter-bot?style=social)](https://github.com/transitive-bullshit/chatgpt-twitter-bot) 55 | - Twitter bot powered by OpenAI's ChatGPT 56 | - [Over 90k followers](https://twitter.com/ChatGPTBot)! 57 | 58 | ### Extensions 59 | 60 | - [chatgpt-google-extension](https://github.com/wong2/chatgpt-google-extension) [![](https://img.shields.io/github/stars/wong2/chatgpt-google-extension?style=social)](https://github.com/wong2/chatgpt-google-extension) 61 | - Browser extension which shows ChatGPT results alongside Google search results 62 | 63 | ### Applications 64 | 65 | - [Ben's Bites AI Search](https://github.com/transitive-bullshit/bens-bites-ai-search) [![](https://img.shields.io/github/stars/transitive-bullshit/bens-bites-ai-search?style=social)](https://github.com/transitive-bullshit/bens-bites-ai-search) 66 | - AI search for all the best resources in AI – powered by Ben's Bites 💯 67 | - [PandoraAI](https://github.com/waylaidwanderer/PandoraAI) [![](https://img.shields.io/github/stars/waylaidwanderer/PandoraAI?style=social)](https://github.com/waylaidwanderer/PandoraAI) 68 | - Web chat client powered by [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api), allowing users to easily chat with multiple AI systems while also offering support for custom presets 69 | - With its seamless and convenient design, PandoraAI provides an engaging conversational AI experience 70 | - [yt-semantic-search](https://github.com/transitive-bullshit/yt-semantic-search) [![](https://img.shields.io/github/stars/transitive-bullshit/yt-semantic-search?style=social)](https://github.com/transitive-bullshit/yt-semantic-search) 71 | - OpenAI-powered semantic search for any YouTube playlist 72 | - [Demo featuring the All-In Podcast](https://all-in-on-ai.vercel.app/) 💪 73 | - [chathub](https://github.com/chathub-dev/chathub) [![](https://img.shields.io/github/stars/chathub-dev/chathub?style=social)](https://github.com/chathub-dev/chathub) 74 | - All-in-one chatbot client 75 | 76 | ## License 77 | 78 | NOTE: this community is not affiliated with OpenAI in any way. 79 | 80 | This website is [open source](${config.githubRepoUrl}). MIT © [${config.author}](${config.twitterUrl}) 81 | ` 82 | 83 | export default function AboutPage({ 84 | content 85 | }: InferGetStaticPropsType) { 86 | return ( 87 | 88 |
89 |
90 |

{config.title}

91 |
92 | 93 | 94 |
95 |
96 | ) 97 | } 98 | 99 | export const getStaticProps = async () => { 100 | const content = await markdownToHtml(markdownContent) 101 | 102 | return { 103 | props: { 104 | content 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pages/about/styles.module.css: -------------------------------------------------------------------------------- 1 | .aboutPage { 2 | flex: 1; 3 | width: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | padding-top: 72px; 9 | gap: var(--gap-2); 10 | } 11 | 12 | .meta { 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | margin: 1em auto; 18 | } 19 | 20 | .title { 21 | font-size: 2.5rem; 22 | } 23 | -------------------------------------------------------------------------------- /pages/index.module.css: -------------------------------------------------------------------------------- 1 | .homePage { 2 | flex: 1; 3 | width: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | padding-top: var(--gap-h); 9 | gap: var(--gap-2); 10 | } 11 | 12 | .body { 13 | position: relative; 14 | flex: 1; 15 | width: 100%; 16 | max-width: 800px; 17 | margin: 0 auto; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | gap: var(--gap); 23 | } 24 | 25 | .section { 26 | position: relative; 27 | width: 100%; 28 | min-height: 100vh; 29 | display: flex; 30 | flex-direction: column; 31 | } 32 | 33 | .hero { 34 | justify-content: center; 35 | align-items: center; 36 | user-select: none; 37 | gap: 2em; 38 | } 39 | 40 | .title { 41 | font-family: 'Source Code Pro', 'Inter', monospace, sans-serif; 42 | text-transform: uppercase; 43 | color: #fff; 44 | text-align: center; 45 | font-size: calc(max(32px, min(128px, 18vw))); 46 | line-height: 1; 47 | 48 | /* text-shadow: 0 0 8px rgba(255, 255, 255, 1); */ 49 | text-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 50 | } 51 | 52 | .desc { 53 | font-style: italic; 54 | text-align: center; 55 | line-height: 1.6; 56 | } 57 | 58 | .discordInfo { 59 | text-align: center; 60 | line-height: 1.6; 61 | } 62 | 63 | .reverse { 64 | display: inline-block; 65 | transform: translateX(0.05em) scaleX(-1); 66 | } 67 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import cs from 'clsx' 3 | // import humanizeNumber from 'human-number' 4 | import { Source_Code_Pro } from 'next/font/google' 5 | 6 | import * as config from '@/lib/config' 7 | // import { HeroButton } from '@/components/HeroButton/HeroButton' 8 | import { Button } from '@/components/Button/Button' 9 | import { Layout } from '@/components/Layout/Layout' 10 | import { MetaballVisualization } from '@/components/MetaballVisualization/MetaballVisualization' 11 | import { PageHead } from '@/components/PageHead/PageHead' 12 | import WebGLSupportChecker from '@/components/WebGLSupportChecker/WebGLSupportChecker' 13 | import { Metaballs } from '@/store/metaballs' 14 | 15 | import styles from './index.module.css' 16 | 17 | const sourceCodePro = Source_Code_Pro({ subsets: ['latin'] }) 18 | 19 | export default function HomePage({ 20 | numMembers, 21 | numMembersOnline 22 | }: { 23 | numMembers: string 24 | numMembersOnline: string 25 | }) { 26 | const [hasMounted, setHasMounted] = React.useState(false) 27 | React.useEffect(() => { 28 | setHasMounted(true) 29 | }, []) 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | {hasMounted && ( 37 | 40 | WebGL is not supported in your browser. Visualization is 41 | disabled. 42 |

43 | } 44 | > 45 | 46 |
47 | )} 48 | 49 |
50 |
51 |
52 |

53 | CHATGPT HAC 54 | K 55 | ERS 56 |

57 | 58 |

59 | Join thousands of developers, researchers, and AI enthusiasts 60 | who are building at the cutting edge of AI 61 |

62 | 63 |
64 |

Discord Members: {numMembers}

65 | 66 |

Currently Online: {numMembersOnline}

67 |
68 | 69 | 76 |
77 |
78 |
79 |
80 |
81 | ) 82 | } 83 | 84 | export async function getStaticProps() { 85 | let numMembers = 7300 86 | let numMembersOnline = 100 87 | 88 | try { 89 | const discordInviteCode = config.discordUrl.split('/').pop() 90 | const res = await fetch( 91 | `https://discord.com/api/v9/invites/${discordInviteCode}?with_counts=true&with_expiration=true` 92 | ) 93 | 94 | const response = await res.json() 95 | numMembers = parseInt(response.approximate_member_count) || 7300 96 | numMembersOnline = parseInt(response.approximate_presence_count) || 100 97 | } catch (err) { 98 | console.error('error fetching discord info', err) 99 | } 100 | 101 | return { 102 | props: { 103 | numMembers, 104 | numMembersOnline 105 | }, 106 | // update counts lazily at most every 10 minutes (in seconds) 107 | revalidate: 10 * 60 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/logo-dark.png -------------------------------------------------------------------------------- /public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/logo-light.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /api/* 4 | -------------------------------------------------------------------------------- /public/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/chatgpt-hackers/8714eb7a73f2d1bb06d51849015e031302b88709/public/social.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Hackers Community 2 | 3 | > Website for the [ChatGPT Hackers community](https://www.chatgpthackers.dev). 4 | 5 | [![Build Status](https://github.com/transitive-bullshit/chatgpt-hackers/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/chatgpt-hackers/actions/workflows/test.yml) [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/transitive-bullshit/chatgpt-hackers/blob/main/license) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 6 | 7 | - [Intro](#intro) 8 | - [License](#license) 9 | 10 | ## Intro 11 | 12 | This is the source of the [ChatGPT Hackers community website](https://www.chatgpthackers.dev). 13 | 14 | Join thousands of other developers, researchers, and AI enthusiasts who are building at the cutting edge of AI! 15 | 16 | ## License 17 | 18 | MIT © [Travis Fischer](https://transitivebullsh.it) 19 | 20 | NOTE: this community is not affiliated with OpenAI in any way. 21 | 22 | If you found this project interesting, please consider [sponsoring me](https://github.com/sponsors/transitive-bullshit) or following me on twitter twitter 23 | -------------------------------------------------------------------------------- /store/metaballs.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createContainer } from 'unstated-next' 3 | 4 | import type { MetaballViz } from '@/components/MetaballVisualization/MetaballViz' 5 | 6 | function useMetaballs() { 7 | const metaballVizRef = React.useRef(null) 8 | 9 | return { 10 | metaballVizRef 11 | } 12 | } 13 | 14 | export const Metaballs = createContainer(useMetaballs) 15 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | /* For rgb(255 115 179 / ) */ 8 | --color-fg-0: 36 41 47; 9 | --color-fg-1: 51 65 85; 10 | --color-fg-2: 71 85 105; 11 | --color-fg-3: 100 116 139; 12 | --color-fg-4: 148 163 184; 13 | 14 | --color-bg-0: 255 255 255; 15 | --color-bg-1: 241 245 249; 16 | --color-bg-2: 226 232 240; 17 | --color-bg-3: 203 213 225; 18 | --color-bg-4: 148 163 184; 19 | } 20 | 21 | .dark { 22 | --color-fg-0: 255 255 255; 23 | /* --color-fg-1: 241 245 249; */ 24 | --color-fg-1: 226 232 240; 25 | --color-fg-2: 203 213 225; 26 | --color-fg-3: 148 163 184; 27 | --color-fg-4: 100 116 139; 28 | 29 | --color-bg-0: 18 18 18; 30 | --color-bg-1: 51 65 85; 31 | --color-bg-2: 71 85 105; 32 | --color-bg-3: 100 116 139; 33 | --color-bg-4: 148 163 184; 34 | } 35 | 36 | .link { 37 | display: inline-block; 38 | text-decoration: none; 39 | line-height: 1.3; 40 | position: relative; 41 | transition: unset; 42 | opacity: 1; 43 | border-color: var(--fg-color-2); 44 | border-bottom-width: 0.135rem; 45 | background: transparent; 46 | background-origin: border-box; 47 | background-repeat: no-repeat; 48 | background-position: 50% 100%; 49 | background-size: 0 0.135rem; 50 | } 51 | 52 | .link:focus, 53 | .link:hover { 54 | text-decoration: none; 55 | border-bottom-color: transparent; 56 | 57 | background-image: linear-gradient(90.68deg, #b439df 0.26%, #e5337e 102.37%); 58 | background-repeat: no-repeat; 59 | background-position: 0 100%; 60 | background-size: 100% 0.135rem; 61 | 62 | transition-property: background-position, background-size; 63 | transition-duration: 300ms; 64 | } 65 | } 66 | 67 | * { 68 | box-sizing: border-box; 69 | } 70 | 71 | svg { 72 | box-sizing: border-box; 73 | } 74 | 75 | a { 76 | color: inherit; 77 | text-decoration: none; 78 | } 79 | 80 | html, 81 | body { 82 | padding: 0; 83 | margin: 0; 84 | font-family: 85 | -apple-system, 86 | BlinkMacSystemFont, 87 | Segoe UI, 88 | Roboto, 89 | Oxygen, 90 | Ubuntu, 91 | Cantarell, 92 | Fira Sans, 93 | Droid Sans, 94 | Helvetica Neue, 95 | sans-serif; 96 | } 97 | 98 | h1, 99 | h2, 100 | h3, 101 | h4, 102 | h5, 103 | h6 { 104 | margin: 0; 105 | padding: 0; 106 | } 107 | 108 | :root { 109 | --bg-color: #fff; 110 | --bg-color-0: rgba(135, 131, 120, 0.15); 111 | --bg-color-1: rgb(247, 246, 243); 112 | --bg-color-2: rgba(135, 131, 120, 0.15); 113 | 114 | --fg-color: #24292f; 115 | --fg-color-1: #292d32; 116 | --fg-color-2: #31363b; 117 | 118 | --max-width: 1200px; 119 | --max-body-width: 1024px; 120 | 121 | --gap: calc(max(8px, min(24px, 1.5vw))); 122 | --gap-w: calc(max(12px, min(48px, 1.5vw))); 123 | --gap-w-1: calc(max(2px, min(24px, 0.7vw))); 124 | --gap-h: calc(max(12px, min(48px, 4vh))); 125 | --gap-h-1: calc(max(8px, min(24px, 1.5vh))); 126 | --gap-2: calc(max(12px, min(48px, 4vmin))); 127 | } 128 | 129 | .dark { 130 | --bg-color: #121212; 131 | --bg-color-0: rgb(71, 76, 80); 132 | --bg-color-1: rgb(63, 68, 71); 133 | --bg-color-2: rgba(135, 131, 120, 0.15); 134 | 135 | --fg-color: #fff; 136 | --fg-color-1: rgba(255, 255, 255, 0.85); 137 | --fg-color-2: rgba(255, 255, 255, 0.7); 138 | } 139 | 140 | body { 141 | background: var(--bg-color); 142 | color: var(--fg-color); 143 | line-height: 1.3; 144 | } 145 | 146 | .match { 147 | border-radius: 0.35em; 148 | padding: 0.1em 0.25em; 149 | margin: -0.1em -0.25em; 150 | box-decoration-break: clone; 151 | background: #fdf59d; 152 | background: #bbebff; 153 | font-weight: normal; 154 | color: #000; 155 | } 156 | 157 | [data-radix-popper-content-wrapper] { 158 | z-index: 500 !important; 159 | } 160 | 161 | [data-nextjs-scroll-focus-boundary] { 162 | display: contents; 163 | } 164 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme') 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: 'class', 6 | content: [ 7 | './pages/**/*.{js,ts,jsx,tsx}', 8 | './components/**/*.{js,ts,jsx,tsx}' 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ['system-ui', ...fontFamily.sans] 14 | }, 15 | colors: { 16 | fg: { 17 | 0: 'rgb(var(--color-fg-0) / )', 18 | 1: 'rgb(var(--color-fg-1) / )', 19 | 2: 'rgb(var(--color-fg-2) / )', 20 | 3: 'rgb(var(--color-fg-3) / )', 21 | 4: 'rgb(var(--color-fg-4) / )' 22 | }, 23 | bg: { 24 | 0: 'rgb(var(--color-bg-0) / )', 25 | 1: 'rgb(var(--color-bg-1) / )', 26 | 2: 'rgb(var(--color-bg-2) / )', 27 | 3: 'rgb(var(--color-bg-3) / )', 28 | 4: 'rgb(var(--color-bg-4) / )' 29 | } 30 | } 31 | } 32 | }, 33 | plugins: [require('@tailwindcss/typography')] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "strictNullChecks": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------