├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .storybook └── main.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── animation │ ├── fluidResistance.spec.ts │ ├── friction.spec.ts │ ├── gravity.spec.ts │ └── group.spec.ts ├── core │ ├── Math_test.res │ └── Vector_test.res ├── forces │ ├── FluidResistance_test.res │ ├── Force_test.res │ ├── Friction_test.res │ └── Gravity_test.res ├── hooks │ ├── useFriction.spec.tsx │ └── usePrefersReducedMotion.spec.ts ├── interpolate │ ├── Interpolate_box_shadow_test.res │ ├── Interpolate_color_test.res │ ├── Interpolate_transform_test.res │ └── Interpolate_unit_test.res ├── parsers │ ├── Parse_box_shadow_test.res │ ├── Parse_color_test.res │ ├── Parse_number_test.res │ ├── Parse_transform_test.res │ ├── Parse_unit_test.res │ ├── derive-style.spec.ts │ └── pairs.spec.ts ├── rAF │ ├── rAF.spec.ts │ └── update.spec.ts └── test-utils │ └── mockRAF.ts ├── bsconfig.json ├── docs ├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── README.md ├── content │ ├── README.md │ ├── api.md │ ├── core-concepts │ │ ├── README.md │ │ ├── from-to-style-animations.md │ │ ├── the-lifecycle-of-an-animation.md │ │ └── vectors.md │ └── getting-started │ │ ├── README.md │ │ ├── accessible-animations.md │ │ ├── controlling-animation-states.md │ │ ├── faq.md │ │ ├── grouped-animations.md │ │ └── your-first-animation.md ├── formideploy.config.js ├── package.json ├── public │ ├── acceleration_equation.svg │ ├── browserconfig.xml │ ├── favicon │ │ ├── Favicon128.png │ │ ├── Favicon144.png │ │ ├── Favicon32.png │ │ └── Favicon48.png │ ├── fluid_resistance_equation.svg │ ├── fluid_resistance_simulation.svg │ ├── force_equation.svg │ ├── friction_equation.svg │ ├── friction_simulation.svg │ ├── from-to.svg │ ├── gravity_equation.svg │ ├── gravity_simulation.svg │ ├── kinematic_equation.svg │ ├── position_change.svg │ ├── position_equation.svg │ ├── site.webmanifest │ ├── vector.svg │ └── velocity_equation.svg ├── src │ ├── app.js │ ├── assets │ │ ├── anchor.js │ │ ├── anchor.svg │ │ ├── arrow.svg │ │ ├── arrow_left.svg │ │ ├── background_renature.svg │ │ ├── burger.svg │ │ ├── chevron.js │ │ ├── chevron.svg │ │ ├── close.svg │ │ ├── feature-1.svg │ │ ├── feature-2.svg │ │ ├── feature-3.svg │ │ ├── header_triangle.svg │ │ └── logos │ │ │ ├── logo_formidable.svg │ │ │ └── logo_formidable_white.svg │ ├── components │ │ ├── body-copy.js │ │ ├── bounce-animation.js │ │ ├── button.js │ │ ├── footer.js │ │ ├── header.js │ │ ├── home-preview.js │ │ ├── loading.js │ │ ├── mdx.js │ │ ├── navigation.js │ │ ├── scroll-to-top.js │ │ ├── secondary-title.js │ │ ├── section-stack.js │ │ ├── section-title.js │ │ ├── sidebar.js │ │ ├── toggle.js │ │ └── wrapper.js │ ├── constants.js │ ├── google-analytics.js │ ├── google-tag-manager.js │ ├── html.js │ ├── index.js │ ├── screens │ │ ├── 404 │ │ │ ├── 404.js │ │ │ └── index.js │ │ ├── docs │ │ │ ├── article.js │ │ │ ├── header.js │ │ │ └── index.js │ │ ├── gallery │ │ │ ├── gallery-preview.js │ │ │ ├── index.js │ │ │ └── samples │ │ │ │ ├── basic-transform.js │ │ │ │ ├── box-shadow.js │ │ │ │ ├── controlled-animations.js │ │ │ │ ├── grouped-animations.js │ │ │ │ ├── index.js │ │ │ │ ├── multiple-properties.js │ │ │ │ ├── orbit.js │ │ │ │ ├── path-tracing.js │ │ │ │ ├── repeat-type.js │ │ │ │ └── set.js │ │ └── home │ │ │ ├── _content.js │ │ │ ├── features.js │ │ │ ├── get-started.js │ │ │ ├── hero.js │ │ │ ├── index.js │ │ │ ├── more-oss.js │ │ │ ├── npm-copy.js │ │ │ └── preview.js │ ├── styles │ │ ├── global-style.js │ │ ├── mixins.js │ │ └── theme.js │ └── utils │ │ └── live-preview.js ├── static.config.js └── yarn.lock ├── package.json ├── rollup.config.js ├── scripts └── jest-transform-esm.js ├── setupTests.ts ├── src ├── animation │ ├── configDefaults.ts │ ├── fluidResistance.ts │ ├── friction.ts │ ├── gravity.ts │ ├── gravity2D.ts │ ├── group.ts │ ├── index.ts │ └── types.ts ├── core │ ├── Math.bs.js │ ├── Math.gen.tsx │ ├── Math.res │ ├── Math.resi │ ├── Vector.bs.js │ ├── Vector.gen.tsx │ ├── Vector.res │ ├── Vector.resi │ └── index.ts ├── forces │ ├── FluidResistance.bs.js │ ├── FluidResistance.gen.tsx │ ├── FluidResistance.res │ ├── FluidResistance.resi │ ├── Force.bs.js │ ├── Force.gen.tsx │ ├── Force.res │ ├── Force.resi │ ├── Friction.bs.js │ ├── Friction.gen.tsx │ ├── Friction.res │ ├── Friction.resi │ ├── Gravity.bs.js │ ├── Gravity.gen.tsx │ ├── Gravity.res │ ├── Gravity.resi │ └── index.ts ├── hooks │ ├── index.ts │ ├── shared.ts │ ├── useFluidResistance.ts │ ├── useFluidResistanceGroup.ts │ ├── useForceGroup.ts │ ├── useFriction.ts │ ├── useFrictionGroup.ts │ ├── useGravity.ts │ ├── useGravity2D.ts │ ├── useGravityGroup.ts │ └── usePrefersReducedMotion.ts ├── index.ts ├── interpolaters │ ├── Interpolate_box_shadow.bs.js │ ├── Interpolate_box_shadow.gen.tsx │ ├── Interpolate_box_shadow.res │ ├── Interpolate_box_shadow.resi │ ├── Interpolate_color.bs.js │ ├── Interpolate_color.gen.tsx │ ├── Interpolate_color.res │ ├── Interpolate_color.resi │ ├── Interpolate_transform.bs.js │ ├── Interpolate_transform.gen.tsx │ ├── Interpolate_transform.res │ ├── Interpolate_transform.resi │ ├── Interpolate_unit.bs.js │ ├── Interpolate_unit.gen.tsx │ ├── Interpolate_unit.res │ ├── Interpolate_unit.resi │ └── index.ts ├── parsers │ ├── LICENSE │ ├── Parse_box_shadow.bs.js │ ├── Parse_box_shadow.gen.tsx │ ├── Parse_box_shadow.res │ ├── Parse_box_shadow.resi │ ├── Parse_color.bs.js │ ├── Parse_color.gen.tsx │ ├── Parse_color.res │ ├── Parse_color.resi │ ├── Parse_number.bs.js │ ├── Parse_number.gen.tsx │ ├── Parse_number.res │ ├── Parse_number.resi │ ├── Parse_transform.bs.js │ ├── Parse_transform.gen.tsx │ ├── Parse_transform.res │ ├── Parse_transform.resi │ ├── Parse_unit.bs.js │ ├── Parse_unit.gen.tsx │ ├── Parse_unit.res │ ├── Parse_unit.resi │ ├── derive-style.ts │ ├── derive-transform.ts │ ├── index.ts │ ├── normalize-color.ts │ └── pairs.ts └── rAF │ ├── index.ts │ ├── rAF.ts │ └── update.ts ├── stories ├── components │ ├── Button.tsx │ ├── Toggle.tsx │ ├── button.css │ └── toggle.css ├── index.css ├── useFluidResistance.stories.tsx ├── useFluidResistanceGroup.stories.tsx ├── useFriction.stories.tsx ├── useFrictionGroup.stories.tsx ├── useGravity.stories.tsx ├── useGravity2D.stories.tsx ├── useGravityGroup.stories.tsx └── utils │ └── index.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-typescript" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | scripts 3 | dist 4 | docs 5 | *.bs.js 6 | *.gen.tsx 7 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:import/errors', 8 | 'plugin:import/warnings', 9 | 'plugin:import/typescript', 10 | 'prettier', 11 | 'prettier/@typescript-eslint', 12 | ], 13 | plugins: ['@typescript-eslint', 'react-hooks', 'jest'], 14 | settings: { 15 | react: { 16 | version: 'detect', 17 | }, 18 | }, 19 | env: { 20 | 'jest/globals': true, 21 | }, 22 | rules: { 23 | 'no-console': ['error', { allow: ['warn', 'error'] }], 24 | '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '_' }], 25 | '@typescript-eslint/explicit-function-return-type': 'off', 26 | '@typescript-eslint/no-empty-function': 'off', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/ban-ts-ignore': 'off', 29 | 'react/no-unescaped-entities': 'off', 30 | 'react/prop-types': 'off', 31 | 'react-hooks/rules-of-hooks': 'error', 32 | 'react-hooks/exhaustive-deps': 'warn', 33 | 'jest/no-disabled-tests': 'warn', 34 | 'jest/no-focused-tests': 'error', 35 | 'jest/no-identical-title': 'error', 36 | 'jest/prefer-to-have-length': 'warn', 37 | 'jest/valid-expect': 'error', 38 | 'import/newline-after-import': 2, 39 | 'import/order': [ 40 | 'error', 41 | { 42 | 'newlines-between': 'always', 43 | }, 44 | ], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: renature CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - saturn 7 | pull_request: 8 | branches: 9 | - saturn 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install Dependencies 24 | run: yarn --frozen-lockfile --non-interactive 25 | - name: Build ReScript and Test 26 | run: | 27 | yarn build:res 28 | yarn test 29 | - name: Check TypeScript 30 | run: yarn check:ts 31 | - name: Lint 32 | run: yarn lint 33 | - name: Build 34 | run: yarn build 35 | 36 | docs: 37 | # TODO: Switch to `ubuntu-latest` when GH internal migration complete 38 | # because it has awscli@2 built in. 39 | # See `ubuntu-latest` note in: https://docs.github.com/en/actions/reference/specifications-for-github-hosted-runners#supported-runners-and-hardware-resources 40 | runs-on: ubuntu-20.04 41 | strategy: 42 | matrix: 43 | node-version: [12.x] 44 | defaults: 45 | run: 46 | working-directory: docs 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Use Node.js ${{ matrix.node-version }} 50 | uses: actions/setup-node@v1 51 | with: 52 | node-version: 12.x 53 | - name: AWS CLI version 54 | run: "aws --version" 55 | - name: Install Dependencies 56 | run: yarn --frozen-lockfile --non-interactive 57 | - name: Quality checks 58 | run: yarn run check-ci 59 | - name: Build docs 60 | run: | 61 | yarn run clean 62 | yarn run build 63 | 64 | - name: Deploy docs (staging) 65 | if: github.ref != 'refs/heads/saturn' 66 | run: yarn run deploy:stage 67 | env: 68 | FORMIDEPLOY_GIT_SHA: ${{ github.event.pull_request.head.sha }} 69 | GITHUB_DEPLOYMENT_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | SURGE_LOGIN: ${{ secrets.SURGE_LOGIN }} 71 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 72 | 73 | - name: Deploy docs (production) 74 | if: github.ref == 'refs/heads/saturn' 75 | run: yarn run deploy:prod 76 | env: 77 | GITHUB_DEPLOYMENT_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 79 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | .merlin 4 | __tests__/**/*.bs.js 5 | .vscode 6 | dist 7 | .DS_Store 8 | tmp 9 | artifacts 10 | coverage 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !LICENSE 3 | !CHANGELOG.md 4 | !./README.md 5 | !CODE_OF_CONDUCT.md 6 | !CONTRIBUTING.md 7 | !/dist/**/*.{d.ts,js,js.map} 8 | !/src/**/*.{d.ts,ts,tsx,bs.js,res,resi} 9 | !./package.json 10 | !bsconfig.json 11 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/*.stories.@(ts|tsx)'], 3 | addons: ['@storybook/addon-knobs'], 4 | }; 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Formidable 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | renature 3 | 4 |
5 |
6 | 7 | 8 | A physics-based animation library for React inspired by the natural world. 9 | 10 | 11 |
12 |
13 | 14 | Maintenance Status 15 | 16 | 17 | NPM Version 18 | 19 | 20 | Test Status 21 | 22 | 23 | Minified gzip size 24 | 25 | 26 |
27 |
28 |
29 | 30 | # `renature` 31 | 32 | `renature` is a physics-based animation library for React focused on modeling natural world forces like gravity, friction, and fluid dynamics, exposed as a set of React hooks. 33 | 34 | ## ✨Features 35 | 36 | - 🎣 A small set of declarative React hooks for animating with ease. 37 | - 🌌 Support for non-traditional physics-based animations using gravity, friction, fluid resistance, and more. 38 | - 🧮 Mathematically accurate and type-safe physics, powered by [ReScript](https://rescript-lang.org/). 39 | - 🔁 Start, stop, delay, and loop animations with our Controller API. 40 | - 0️⃣ A tiny animation library with zero dependencies! 41 | 42 | ## 📃Documentation 43 | 44 | `renature`'s documentation lives on our [docs site](https://formidable.com/open-source/renature/). Notice something inaccurate or confusing? Feel free [to open an issue](https://github.com/FormidableLabs/renature/issues) or [make a pull request](https://github.com/FormidableLabs/renature/pulls) to help improve the documentation for everyone! The source for our docs site lives in this repo in the [`docs`](/docs/README.md) folder. 45 | 46 | ## Maintenance Status 47 | 48 | **Archived:** This project is no longer maintained by Formidable. We are no longer responding to issues or pull requests unless they relate to security concerns. We encourage interested developers to fork this project and make it their own! 49 | -------------------------------------------------------------------------------- /__tests__/core/Math_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Math", () => { 6 | describe("constrainf", () => { 7 | it("should constrain a value above the range to the upper limit of the range", () => { 8 | open Expect 9 | expect(Math.constrainf(~low=10., ~high=50., 70.)) |> toEqual(50.) 10 | }) 11 | 12 | it("should constrain a value below the range to the lower limit of the range", () => { 13 | open Expect 14 | expect(Math.constrainf(~low=10., ~high=50., 1.)) |> toEqual(10.) 15 | }) 16 | 17 | it("should leave values within the range untouched", () => { 18 | open Expect 19 | expect(Math.constrainf(~low=10., ~high=50., 40.)) |> toEqual(40.) 20 | }) 21 | }) 22 | 23 | describe("lerpf", () => it("should linear interpolate between float values", () => { 24 | open Expect 25 | expect(Math.lerpf(~acc=5., ~target=25., ~roundness=0.35)) |> toEqual(12.) 26 | })) 27 | 28 | describe("remapf", () => { 29 | it("should map a value on an input range to a value on an output domain", () => { 30 | open Expect 31 | expect(Math.remapf(~range=(0., 300.), ~domain=(0., 1.), ~value=150.)) |> toEqual(0.5) 32 | }) 33 | 34 | it("should handle ranges and domains with non-overlapping inputs", () => { 35 | open Expect 36 | expect(Math.remapf(~range=(200., 300.), ~domain=(10., 15.), ~value=220.)) |> toEqual(11.) 37 | }) 38 | 39 | it("should extrapolate outside the input range", () => { 40 | open Expect 41 | expect(Math.remapf(~range=(200., 300.), ~domain=(10., 15.), ~value=180.)) |> toEqual(9.) 42 | }) 43 | 44 | it("should handle a negative input range", () => { 45 | open Expect 46 | expect(Math.remapf(~range=(-200., -300.), ~domain=(10., 15.), ~value=-220.)) |> toEqual(11.) 47 | }) 48 | }) 49 | 50 | describe("normalizef", () => { 51 | it("should map a number on an input range to a value in the domain [0, 1]", () => { 52 | open Expect 53 | expect(Math.normalizef(~range=(0., 300.), ~value=150.)) |> toEqual(0.5) 54 | }) 55 | 56 | it("should extrapolate outside the input range", () => { 57 | open Expect 58 | expect(Math.normalizef(~range=(200., 300.), ~value=150.)) |> toEqual(-0.5) 59 | }) 60 | 61 | it("should handle a negative input range", () => { 62 | open Expect 63 | expect(Math.normalizef(~range=(-200., -300.), ~value=-250.)) |> toEqual(0.5) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/forces/FluidResistance_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("FluidResistance", () => { 6 | describe("fluidResistanceForceMag", () => 7 | it("should derive the magnitude of the drag force", () => { 8 | open Expect 9 | expect( 10 | FluidResistance.fluidResistanceForceMag(~rho=25., ~velocity=(5., 0.), ~area=2., ~cDrag=0.1), 11 | ) |> toEqual(62.5) 12 | }) 13 | ) 14 | 15 | describe("fluidResistanceForceV", () => { 16 | let rho = 25. 17 | let velocity = (0., 5.) 18 | let area = 2. 19 | let cDrag = 0.1 20 | 21 | it("should correctly derive the vector of the drag force in two dimensions", () => { 22 | open Expect 23 | expect( 24 | FluidResistance.fluidResistanceForceV(~rho, ~velocity, ~area, ~cDrag) |> fst, 25 | ) |> toEqual(-0.) 26 | }) 27 | 28 | it("should correctly derive the vector of the drag force in two dimensions", () => { 29 | open Expect 30 | expect( 31 | FluidResistance.fluidResistanceForceV(~rho, ~velocity, ~area, ~cDrag) |> snd, 32 | ) |> toEqual(-62.5) 33 | }) 34 | }) 35 | 36 | describe("getFluidPositionAtTerminalVelocity", () => { 37 | let mass = 10. 38 | let rho = 25. 39 | let area = 2. 40 | let cDrag = 0.1 41 | 42 | it( 43 | "should correctly derive the position of the mover when it reaches terminal velocity", 44 | () => { 45 | open Expect 46 | expect( 47 | FluidResistance.getFluidPositionAtTerminalVelocity(~mass, ~rho, ~cDrag, ~area), 48 | ) |> toBeSoCloseTo(65.05, ~digits=2) 49 | }, 50 | ) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /__tests__/forces/Force_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Force", () => describe("applyForce", () => { 6 | let force = (10., 5.) 7 | let entity = { 8 | open Force 9 | { 10 | acceleration: (0., 0.), 11 | velocity: (0., 0.), 12 | position: (0., 0.), 13 | mass: 100., 14 | } 15 | } 16 | 17 | it("should alter an entity's x acceleration", () => { 18 | open Expect 19 | expect(Force.applyForce(~force, ~entity, ~time=0.001).acceleration |> fst) |> toBeCloseTo(0.1) 20 | }) 21 | 22 | it("should alter an entity's y acceleration", () => { 23 | open Expect 24 | expect(Force.applyForce(~force, ~entity, ~time=0.001).acceleration |> snd) |> toBeCloseTo( 25 | 0.05, 26 | ) 27 | }) 28 | 29 | it("should alter an entity's x velocity", () => { 30 | open Expect 31 | expect(Force.applyForce(~force, ~entity, ~time=0.001).velocity |> fst) |> toBeCloseTo(0.0001) 32 | }) 33 | 34 | it("should alter an entity's y velocity", () => { 35 | open Expect 36 | expect(Force.applyForce(~force, ~entity, ~time=0.001).velocity |> snd) |> toBeCloseTo(0.00005) 37 | }) 38 | 39 | it("should alter an entity's x position", () => { 40 | open Expect 41 | expect(Force.applyForce(~force, ~entity, ~time=0.001).position |> fst) |> toBeCloseTo( 42 | 0.00000001, 43 | ) 44 | }) 45 | 46 | it("should alter an entity's y position", () => { 47 | open Expect 48 | expect(Force.applyForce(~force, ~entity, ~time=0.001).position |> snd) |> toBeCloseTo( 49 | 0.000000005, 50 | ) 51 | }) 52 | })) 53 | -------------------------------------------------------------------------------- /__tests__/forces/Friction_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Friction", () => { 6 | describe("frictionForceMag", () => 7 | it("should derive the magnitude of the frictional force", () => { 8 | open Expect 9 | expect(Friction.frictionForceMag(~mu=0.01, ~mass=1.)) |> toEqual(0.01 *. Gravity.gE) 10 | }) 11 | ) 12 | 13 | describe("frictionForceV", () => { 14 | let mu = 0.01 15 | let mass = 15. 16 | let velocity = (0.05, 0.1) 17 | 18 | it("should correctly derive the vector of the force of friction in two dimensions", () => { 19 | open Expect 20 | expect(Friction.frictionForceV(~mu, ~mass, ~velocity) |> fst) |> toBeCloseTo(-0.65785) 21 | }) 22 | 23 | it("should correctly derive the vector of the force of friction in two dimensions", () => { 24 | open Expect 25 | expect(Friction.frictionForceV(~mu, ~mass, ~velocity) |> snd) |> toBeCloseTo(-1.31568) 26 | }) 27 | }) 28 | 29 | describe("getMaxDistanceFriction", () => 30 | it( 31 | "should derive the max distance an object can travel before coming to rest based on an initial velocity and a constant acceleration", 32 | () => { 33 | open Expect 34 | expect(Friction.getMaxDistanceFriction(~mu=0.1, ~initialVelocity=50.)) |> toBeCloseTo( 35 | 1274.645266, 36 | ) 37 | }, 38 | ) 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /__tests__/hooks/usePrefersReducedMotion.spec.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | 3 | import { usePrefersReducedMotion } from '../../src/hooks/usePrefersReducedMotion'; 4 | 5 | describe('usePrefersReducedMotion', () => { 6 | afterAll(() => { 7 | // jsdom does not implement window.matchMedia, so we mock it in this suite. 8 | // Ensure this is reset to undefined at the end of the suite. 9 | // TS will complain about the assignment to undefined, but we can safely ignore it here. 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 11 | // @ts-expect-error 12 | window.matchMedia = undefined; 13 | }); 14 | 15 | it.each([ 16 | { query: 'no-preference', prefersReducedMotion: false }, 17 | { query: 'reduce', prefersReducedMotion: true }, 18 | ])( 19 | 'should return a boolean indicating whether or not the user prefers reduced motion', 20 | ({ query, prefersReducedMotion }) => { 21 | window.matchMedia = jest.fn().mockImplementation((q) => ({ 22 | matches: q === `(prefers-reduced-motion: ${query})`, 23 | media: '', 24 | onchange: jest.fn(), 25 | addEventListener: jest.fn(), 26 | removeEventListener: jest.fn(), 27 | })); 28 | 29 | const { result } = renderHook(() => usePrefersReducedMotion()); 30 | expect(result.current).toBe(prefersReducedMotion); 31 | } 32 | ); 33 | 34 | it.each([ 35 | { query: 'no-preference', prefersReducedMotion: false }, 36 | { query: 'reduce', prefersReducedMotion: true }, 37 | ])( 38 | 'should set up listeners for changes to the reduced motion preference', 39 | ({ query, prefersReducedMotion }) => { 40 | const addEventListenerMock = jest.fn(); 41 | const removeEventListenerMock = jest.fn(); 42 | 43 | window.matchMedia = jest.fn().mockImplementation((q) => ({ 44 | matches: q === `(prefers-reduced-motion: ${query})`, 45 | media: '', 46 | onchange: jest.fn(), 47 | addEventListener: addEventListenerMock, 48 | removeEventListener: removeEventListenerMock, 49 | })); 50 | 51 | const { result, unmount } = renderHook(() => usePrefersReducedMotion()); 52 | expect(result.current).toBe(prefersReducedMotion); 53 | 54 | expect(addEventListenerMock).toHaveBeenCalledTimes(1); 55 | 56 | unmount(); 57 | 58 | expect(addEventListenerMock).toHaveBeenCalledTimes(1); 59 | expect(removeEventListenerMock).toHaveBeenCalledTimes(1); 60 | } 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/interpolate/Interpolate_box_shadow_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Interpolate_box_shadow", () => { 6 | describe("interpolateBoxShadow", () => { 7 | it("should interpolate a box shadow from one value to another", () => { 8 | let from = "0px 0px rgba(0, 0, 0, 1)" 9 | let to_ = "10px 5px rgba(255, 255, 255, 0.4)" 10 | 11 | open Expect 12 | expect( 13 | Interpolate_box_shadow.interpolateBoxShadow( 14 | ~range=(0., 150.), 15 | ~domain=(from, to_), 16 | ~value=75., 17 | ), 18 | ) |> toEqual("5px 2.5px 0px 0px rgba(127, 127, 127, 0.7)") 19 | }) 20 | 21 | it("should handle box shadows of different shorthand styles", () => { 22 | let from = "0px 0px 6px 4px #64C88C" 23 | let to_ = "10px 5px rgba(255, 255, 255, 0.4)" 24 | 25 | open Expect 26 | expect( 27 | Interpolate_box_shadow.interpolateBoxShadow( 28 | ~range=(0., 150.), 29 | ~domain=(from, to_), 30 | ~value=75., 31 | ), 32 | ) |> toEqual("5px 2.5px 3px 2px rgba(177, 227, 197, 0.7)") 33 | }) 34 | 35 | it("should handle box shadows with an inset property", () => { 36 | let from = "inset 0px 0px 6px 4px rgba(0, 0, 0, 0)" 37 | let to_ = "inset 10px 5px 10px rgba(200, 200, 200, 0.4)" 38 | 39 | open Expect 40 | expect( 41 | Interpolate_box_shadow.interpolateBoxShadow( 42 | ~range=(0., 150.), 43 | ~domain=(from, to_), 44 | ~value=75., 45 | ), 46 | ) |> toEqual("inset 5px 2.5px 8px 2px rgba(100, 100, 100, 0.2)") 47 | }) 48 | 49 | it( 50 | "should default none box shadows to a 0px 0px 0px 0px rgba(0, 0, 0, 1) for interpolation", 51 | () => { 52 | let from = "-10px -20px 6px 4px rgba(100, 100, 100, 0)" 53 | let to_ = "none" 54 | 55 | open Expect 56 | expect( 57 | Interpolate_box_shadow.interpolateBoxShadow( 58 | ~range=(0., 150.), 59 | ~domain=(from, to_), 60 | ~value=75., 61 | ), 62 | ) |> toEqual("-5px -10px 3px 2px rgba(50, 50, 50, 0.5)") 63 | }, 64 | ) 65 | }) 66 | 67 | describe("interpolateBoxShadow", () => it("should interpolate multiple box-shadows", () => { 68 | let from = "0px 0px rgba(0, 0, 0, 1), 10px 5px rgba(200, 200, 200, 0)" 69 | let to_ = "10px 5px rgba(255, 255, 255, 0.4), 5px 0px 10px 20px rgba(50, 50, 50, 1)" 70 | 71 | open Expect 72 | expect( 73 | Interpolate_box_shadow.interpolateBoxShadows( 74 | ~range=(0., 150.), 75 | ~domain=(from, to_), 76 | ~value=75., 77 | ), 78 | ) |> toEqual( 79 | "5px 2.5px 0px 0px rgba(127, 127, 127, 0.7), 7.5px 2.5px 5px 10px rgba(125, 125, 125, 0.5)", 80 | ) 81 | })) 82 | }) 83 | -------------------------------------------------------------------------------- /__tests__/interpolate/Interpolate_color_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Interpolate_color", () => { 6 | describe("lerpColor", () => it("should linearly interpolate between two colors", () => { 7 | let a = { 8 | open Parse_color 9 | {r: 0., g: 0., b: 0., a: 0.} 10 | } 11 | 12 | let b = { 13 | open Parse_color 14 | {r: 100., g: 200., b: 255., a: 0.6} 15 | } 16 | 17 | open Expect 18 | expect(Interpolate_color.lerpColor(~acc=a, ~target=b, ~roundness=0.5)) |> toEqual({ 19 | open Parse_color 20 | {r: 50., g: 100., b: 127.5, a: 0.3} 21 | }) 22 | })) 23 | 24 | describe("interpolateColor", () => 25 | it("should map an input numeric range to an output color domain", () => { 26 | let a = { 27 | open Parse_color 28 | {r: 0., g: 0., b: 0., a: 0.} 29 | } 30 | 31 | let b = { 32 | open Parse_color 33 | {r: 100., g: 200., b: 255., a: 0.6} 34 | } 35 | 36 | open Expect 37 | expect( 38 | Interpolate_color.interpolateColor(~range=(0., 150.), ~domain=(a, b), ~value=75.), 39 | ) |> toEqual("rgba(50, 100, 127, 0.3)") 40 | }) 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /__tests__/interpolate/Interpolate_unit_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Interpolate_unit", () => describe("interpolateUnit", () => { 6 | it("should interpolate from one unit value to another", () => { 7 | let from = "45px" 8 | let to_ = "100px" 9 | 10 | open Expect 11 | expect( 12 | Interpolate_unit.interpolateUnit(~range=(0., 150.), ~domain=(from, to_), ~value=75.), 13 | ) |> toEqual("72.5px") 14 | }) 15 | 16 | it("should handle percentage units", () => { 17 | let from = "45%" 18 | let to_ = "100%" 19 | 20 | open Expect 21 | expect( 22 | Interpolate_unit.interpolateUnit(~range=(0., 150.), ~domain=(from, to_), ~value=75.), 23 | ) |> toEqual("72.5%") 24 | }) 25 | 26 | it("should handle rem units", () => { 27 | let from = "45rem" 28 | let to_ = "100rem" 29 | 30 | open Expect 31 | expect( 32 | Interpolate_unit.interpolateUnit(~range=(0., 150.), ~domain=(from, to_), ~value=75.), 33 | ) |> toEqual("72.5rem") 34 | }) 35 | 36 | it( 37 | "should just return the number portion of the unit if the unit does not match a known CSS unit", 38 | () => { 39 | let from = "45chonks" 40 | let to_ = "100chonks" 41 | 42 | open Expect 43 | expect( 44 | Interpolate_unit.interpolateUnit(~range=(0., 150.), ~domain=(from, to_), ~value=75.), 45 | ) |> toEqual("72.5") 46 | }, 47 | ) 48 | })) 49 | -------------------------------------------------------------------------------- /__tests__/parsers/Parse_color_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Parse_color", () => describe("testColor", () => { 6 | testAll( 7 | "should identify valid CSS colors", 8 | list{"rgba(0, 0, 0, 1)", "#ffffff", "hsla(200, 100%, 30%, 0.5)", "teal"}, 9 | color => { 10 | open Expect 11 | expect(Parse_color.testColor(color)) |> toBe(true) 12 | }, 13 | ) 14 | 15 | testAll( 16 | "should flag valid CSS colors", 17 | list{"rgba(0, 30%, 0, 1)", "#fffff", "hsla(200, 10, 30%, 0.5)", "yeet"}, 18 | color => { 19 | open Expect 20 | expect(Parse_color.testColor(color)) |> toBe(false) 21 | }, 22 | ) 23 | })) 24 | -------------------------------------------------------------------------------- /__tests__/parsers/Parse_number_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Parse_number", () => { 6 | describe("testNumber", () => { 7 | testAll( 8 | "should identify valid strings that can be represented as numbers, excluding unit values", 9 | list{"5", "0.25"}, 10 | number => { 11 | open Expect 12 | expect(Parse_number.testNumber(number)) |> toBe(true) 13 | }, 14 | ) 15 | 16 | testAll( 17 | "should flag strings that cannot be represented as numbers", 18 | list{"20px", "hey", "oops", "twenty", "one23"}, 19 | number => { 20 | open Expect 21 | expect(Parse_number.testNumber(number)) |> toBe(false) 22 | }, 23 | ) 24 | }) 25 | 26 | describe("parseNumber", () => { 27 | it("should parse strings to their numeric representation", () => { 28 | open Expect 29 | expect(Parse_number.parseNumber("20")) |> toEqual(20.) 30 | }) 31 | 32 | it("should parse strings to their numeric representation – decimals", () => { 33 | open Expect 34 | expect(Parse_number.parseNumber("0.25")) |> toEqual(0.25) 35 | }) 36 | 37 | it("should parse strings to their numeric representation – strip trailing zeros", () => { 38 | open Expect 39 | expect(Parse_number.parseNumber("20.50")) |> toEqual(20.5) 40 | }) 41 | 42 | it("should return NaN for un-parseable strings", () => { 43 | open Expect 44 | expect(Parse_number.parseNumber("yeet")) |> toEqual(Js.Float._NaN) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /__tests__/parsers/Parse_unit_test.res: -------------------------------------------------------------------------------- 1 | open Jest 2 | 3 | let it = test 4 | 5 | describe("Parse_unit", () => { 6 | describe("testUnit", () => { 7 | testAll( 8 | "should identify valid CSS unit values", 9 | list{ 10 | "20px", 11 | "1rem", 12 | "0.5em", 13 | "20%", 14 | "100vw", 15 | "25.50vh", 16 | "360deg", 17 | "22rad", 18 | "0.25turn", 19 | "-20%", 20 | }, 21 | unit_ => { 22 | open Expect 23 | expect(Parse_unit.testUnit(unit_)) |> toBe(true) 24 | }, 25 | ) 26 | 27 | testAll( 28 | "should flag invalid CSS unit values", 29 | list{"20pe", "1oop", "0.5yet", "20", "0.5", "turn20", "deg230"}, 30 | unit_ => { 31 | open Expect 32 | expect(Parse_unit.testUnit(unit_)) |> toBe(false) 33 | }, 34 | ) 35 | }) 36 | 37 | describe("parseUnit", () => { 38 | it("should parse a raw string into a cssUnit record", () => { 39 | open Expect 40 | expect(Parse_unit.parseUnit("20px")) |> toEqual({ 41 | open Parse_unit 42 | {value: 20., unit: Js.Nullable.return("px")} 43 | }) 44 | }) 45 | 46 | it("should mark a unit as null if the string can't be parsed into a cssUnit record", () => { 47 | open Expect 48 | expect(Parse_unit.parseUnit("50rev")) |> toEqual({ 49 | open Parse_unit 50 | {value: 50., unit: Js.Nullable.null} 51 | }) 52 | }) 53 | 54 | it("should mark a value as NaN if the string contains no numbers", () => { 55 | open Expect 56 | expect(Parse_unit.parseUnit("px")) |> toEqual({ 57 | open Parse_unit 58 | { 59 | value: Js.Float._NaN, 60 | unit: Js.Nullable.return("px"), 61 | } 62 | }) 63 | }) 64 | 65 | it( 66 | "should mark a value as NaN and unit as null if the string contains no numbers and no css units", 67 | () => { 68 | open Expect 69 | expect(Parse_unit.parseUnit("heck")) |> toEqual({ 70 | open Parse_unit 71 | {value: Js.Float._NaN, unit: Js.Nullable.null} 72 | }) 73 | }, 74 | ) 75 | 76 | it("should preserve negative numbers", () => { 77 | open Expect 78 | expect(Parse_unit.parseUnit("-2.5rem")) |> toEqual({ 79 | open Parse_unit 80 | {value: -2.5, unit: Js.Nullable.return("rem")} 81 | }) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /__tests__/parsers/derive-style.spec.ts: -------------------------------------------------------------------------------- 1 | import { deriveStyle } from '../../src/parsers'; 2 | 3 | describe('deriveStyle', () => { 4 | it('should derive the style of an animating element from its style property (number)', () => { 5 | const el = document.createElement('div'); 6 | el.style.opacity = '0.5'; 7 | 8 | const { from } = deriveStyle(el, { opacity: 1 }); 9 | 10 | expect(from).toEqual({ opacity: 0.5 }); 11 | }); 12 | 13 | it('should derive the style of an animating element from its style property (string)', () => { 14 | const el = document.createElement('div'); 15 | el.style.width = '25rem'; 16 | 17 | const { from } = deriveStyle(el, { width: '12.5rem' }); 18 | 19 | expect(from).toEqual({ width: '25rem' }); 20 | }); 21 | 22 | it('should use the computed style of an animating element no style can be found on the style property (number)', () => { 23 | const el = document.createElement('div'); 24 | 25 | const getPropertyValueMock = jest.fn(); 26 | const getComputedStyleSpy = jest 27 | .spyOn(window, 'getComputedStyle') 28 | .mockImplementation( 29 | () => 30 | (({ 31 | getPropertyValue: getPropertyValueMock, 32 | } as unknown) as CSSStyleDeclaration) 33 | ); 34 | 35 | deriveStyle(el, { opacity: 0 }); 36 | 37 | expect(getComputedStyleSpy).toHaveBeenCalledWith(el); 38 | expect(getPropertyValueMock).toHaveBeenCalledWith('opacity'); 39 | }); 40 | 41 | it('should build a transfrom with animatable none values to match to values', () => { 42 | const el = document.createElement('div'); 43 | 44 | const { from } = deriveStyle(el, { 45 | transform: 'rotate(360deg) scale(1.5) perspective(200px)', 46 | }); 47 | 48 | expect(from).toEqual({ 49 | transform: 'rotate(0deg) scale(1) perspective(0px)', 50 | }); 51 | }); 52 | 53 | it('should merge currently applied transforms into the from value', () => { 54 | const el = document.createElement('div'); 55 | el.style.transform = 'translateX(100px)'; 56 | 57 | const { from } = deriveStyle(el, { 58 | transform: 'rotate(360deg) scale(1.5) perspective(200px)', 59 | }); 60 | 61 | expect(from).toEqual({ 62 | transform: 'translateX(100px) rotate(0deg) scale(1) perspective(0px)', 63 | }); 64 | }); 65 | 66 | it('should build a transform with animatable none values to match from values', () => { 67 | const el = document.createElement('div'); 68 | el.style.transform = 'translateX(100px) skewY(1rad)'; 69 | 70 | const { from, to } = deriveStyle(el, { 71 | transform: 'rotate(360deg) scale(1.5) perspective(200px)', 72 | }); 73 | 74 | expect(from).toEqual({ 75 | transform: 76 | 'translateX(100px) skewY(1rad) rotate(0deg) scale(1) perspective(0px)', 77 | }); 78 | expect(to).toEqual({ 79 | transform: 80 | 'rotate(360deg) scale(1.5) perspective(200px) translateX(0px) skewY(0rad)', 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /__tests__/rAF/rAF.spec.ts: -------------------------------------------------------------------------------- 1 | import { rAF } from '../../src/rAF'; 2 | 3 | describe('rAF', () => { 4 | let mockFrameId = 0; 5 | const requestAnimationFrameMock: jest.SpyInstance< 6 | number, 7 | [FrameRequestCallback] 8 | > = jest.spyOn(window, 'requestAnimationFrame').mockImplementation(() => { 9 | mockFrameId++; 10 | return mockFrameId; 11 | }); 12 | 13 | afterEach(() => { 14 | mockFrameId = 0; 15 | requestAnimationFrameMock.mockClear(); 16 | }); 17 | 18 | it('should return a start and stop function for initiating and stopping the frame loop', () => { 19 | const { start, stop } = rAF(); 20 | expect(typeof start).toBe('function'); 21 | expect(typeof stop).toBe('function'); 22 | }); 23 | 24 | it('should execute an arbitrary listener function inside the frameloop', () => { 25 | const { start } = rAF(); 26 | 27 | const listener = jest.fn(); 28 | start(listener); 29 | 30 | expect(listener).toHaveBeenCalled(); 31 | }); 32 | 33 | it('should stop the frameloop when stop is called', () => { 34 | const cancelAnimationFrame = jest.spyOn(window, 'cancelAnimationFrame'); 35 | 36 | const { start, stop } = rAF(); 37 | 38 | const listener = jest.fn(); 39 | start(listener); 40 | 41 | stop(); 42 | 43 | expect(cancelAnimationFrame).toHaveBeenCalledWith(1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/test-utils/mockRAF.ts: -------------------------------------------------------------------------------- 1 | type RAFCallback = (timestamp: DOMHighResTimeStamp) => void; 2 | 3 | export class MockRAF { 4 | #now: number; 5 | #callbacks: Record; 6 | #callbacksLength: number; 7 | 8 | constructor() { 9 | this.#now = 0; 10 | this.#callbacks = {}; 11 | this.#callbacksLength = 0; 12 | } 13 | 14 | get now(): number { 15 | return this.#now; 16 | } 17 | 18 | rAF = (cb: RAFCallback): number => { 19 | this.#callbacksLength += 1; 20 | this.#callbacks[this.#callbacksLength] = cb; 21 | 22 | return this.#callbacksLength; 23 | }; 24 | 25 | cancel = (id: number): void => { 26 | delete this.#callbacks[id]; 27 | }; 28 | 29 | step = (opts: { time?: number; count?: number } = {}): void => { 30 | const options = { 31 | time: 1000 / 60, 32 | count: 1, 33 | ...opts, 34 | }; 35 | 36 | let prevCallbacks: Record< 37 | number, 38 | (timestamp: DOMHighResTimeStamp) => void 39 | > = {}; 40 | 41 | for (let i = 0; i < options.count; i++) { 42 | prevCallbacks = this.#callbacks; 43 | this.#callbacks = {}; 44 | 45 | this.#now += options.time; 46 | 47 | Object.keys(prevCallbacks).forEach((id) => { 48 | const callback = prevCallbacks[parseInt(id, 10)]; 49 | callback(this.#now); 50 | }); 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json", 3 | "name": "renature", 4 | "version": "0.6.3", 5 | "sources": [ 6 | { 7 | "dir": "src/core", 8 | "subdirs": true 9 | }, 10 | { 11 | "dir": "src/forces", 12 | "subdirs": true 13 | }, 14 | { 15 | "dir": "src/interpolaters", 16 | "subdirs": true 17 | }, 18 | { 19 | "dir": "src/parsers", 20 | "subdirs": true 21 | }, 22 | { 23 | "dir": "__tests__", 24 | "subdirs": true, 25 | "type": "dev" 26 | } 27 | ], 28 | "package-specs": { 29 | "module": "es6", 30 | "in-source": true 31 | }, 32 | "suffix": ".bs.js", 33 | "bs-dependencies": [], 34 | "bs-dev-dependencies": ["@glennsl/bs-jest"], 35 | "warnings": { 36 | "error": "+5" 37 | }, 38 | "refmt": 3, 39 | "bsc-flags": ["-bs-super-errors"], 40 | "gentypeconfig": { 41 | "language": "typescript" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | // "extends": "react-static/.babelrc", 3 | "presets": ["react-static/babel-preset.js"], 4 | "plugins": ["babel-plugin-styled-components"] 5 | } 6 | -------------------------------------------------------------------------------- /docs/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/* 4 | .eslintrc.js 5 | -------------------------------------------------------------------------------- /docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@babel/eslint-parser', 4 | parserOptions: { 5 | requireConfigFile: false, 6 | babelOptions: { 7 | configFile: './.babelrc', 8 | }, 9 | }, 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:react/recommended', 13 | 'plugin:import/errors', 14 | 'plugin:import/warnings', 15 | 'prettier', 16 | ], 17 | plugins: ['prettier', 'react-hooks'], 18 | env: { 19 | browser: true, 20 | commonjs: true, 21 | es6: true, 22 | node: true, 23 | jest: true, 24 | }, 25 | settings: { 26 | react: { 27 | version: 'detect', 28 | }, 29 | }, 30 | globals: { 31 | expect: true, 32 | }, 33 | rules: { 34 | 'prettier/prettier': ['error'], 35 | quotes: ['error', 'single', { allowTemplateLiterals: true }], 36 | 'no-console': 'error', 37 | 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 38 | 'no-undef': 'error', 39 | 'import/newline-after-import': 2, 40 | 'import/order': [ 41 | 'error', 42 | { 43 | 'newlines-between': 'always', 44 | }, 45 | ], 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /docs/content/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | order: 0 4 | --- 5 | 6 | # Introduction 7 | 8 | `renature` is a physics-based animation library for React inspired by the natural world. Taking influence from other popular physics-based animation libraries like [`react-spring`](https://www.react-spring.io/) and elements of [`framer-motion`](https://www.framer.com/motion/), `renature` focuses on using non-traditional physics primitives like gravity, friction, and fluid resistance. It also provides a simple two-dimensional API for creating visual experiences similar to [Processing](https://processing.org/) or [`p5.js`](https://p5js.org/). 9 | 10 | `renature` focuses on speed, precision, and a small bundle size. The implementation is a mix of [ReScript](https://rescript-lang.org/) and [TypeScript](https://www.typescriptlang.org/). We use ReScript for the vector primitives, parsers, and interpolators, and TypeScript for the hooks implementations and frame loop. We also value being dependency-free — `renature` has no external dependencies! 11 | 12 | ## Documentation 13 | 14 | We hope you love `renature` and enjoy learning more about the library! Check out the following sections to get started: 15 | 16 | - [**Getting Started**](./getting-started.md) – Learn how to install `renature`, use your first hook, and control animation play states. 17 | - [**Core Concepts**](./core-concepts.md) – Interested in the underlying physics and math powering `renature`? Curious to understand the core of how `renature` looks? This section will dive deep into library internals for users and contributors alike. 18 | - [**API**](./api.md) – The formal API documentation for `renature`. Complete with live code examples powered by [`react-live`](https://github.com/FormidableLabs/react-live). 19 | 20 | Can't find what you're looking for? Confused by a particular section of the docs? Catch a typo? Please [open an issue on GitHub](https://github.com/FormidableLabs/renature/issues) or consider [submitting a PR](https://github.com/FormidableLabs/renature/pulls). We love community contributors ❤️. 21 | -------------------------------------------------------------------------------- /docs/content/core-concepts/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core Concepts 3 | order: 2 4 | --- 5 | 6 | # Core Concepts 7 | 8 | In this section, we'll discuss some of the core concepts behind `renature`, including some of the fundmental mathematics that power the library. While it isn't necessary to understand these concepts to use `renature`, it can give you a better understanding of why your animations behave the way they do. It'll also help you gain context on the library if you decide you'd like to contribute – and we hope you will! 9 | 10 | ## Sections 11 | 12 | The Core Concepts section of the docs is split into the following sections: 13 | 14 | - [**Vectors**](./vectors.md) – Learn what vectors are and how we model them in `renature`. 15 | - [**From – To Style Animations**](./from-to-style-animations.md) – The basic hooks in `renature` animate between a `from` and `to` state. Learn how `renature` maps values from an underlying physics simulation to CSS values based on the supplied `from` and `to` states. 16 | - [**The Lifecycle of an Animation**](./the-lifecycle-of-an-animation.md) – `renature` animations have a three stage lifecycle – **Simulate**, **Interpolate**, **Animate**. Learn about each of these lifecycles to better control your animations. 17 | -------------------------------------------------------------------------------- /docs/content/core-concepts/from-to-style-animations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: From – To Style Animations 3 | order: 1 4 | --- 5 | 6 | ## From – To Style Animations 7 | 8 | `renature`'s core hooks, like `useGravity`, `useFriction`, and `useFluidResistance`, operate on the notion of animating **from** a specific CSS state (i.e. `opacity: 0`) **to** a new CSS state (i.e. `opacity: 1`). In this way, the API is quite similar to [`react-spring`](https://www.react-spring.io/) and even traditional CSS `keyframes`. The difference is in the physics we use to determine how you get from one state to another. 9 | 10 | ![renature hooks animate from one CSS state to another.](../../public/from-to.svg) 11 | 12 | Each force in `renature` has its own character and produces its own style of motion; depending on the effect you're trying to achieve, one force might be better suited for your needs than another. However, every force uses the same notion of a `mover` object (the object experiencing the force in our simulation) and a `force` vector. By applying the `force` vector to the `mover` object – using the equations outlined in the [Vectors](./vectors.md) sectopm – we can determine the `position` of the mover for the current frame. 13 | 14 | We then determine where this `position` lies along the whole trajectory of the moving object's path. Is the mover halfway to its target destination? Two thirds of the way? This information helps us to determine what percentage "done" the animation is. It's almost like a physics-based version of a [tween](https://inventingwithmonster.io/20190304-how-to-write-a-tween/). 15 | 16 | Finally, we use a technique called [linear interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) to map this progress to your CSS state. For example, if the `mover` is halfway along its total trajectory, and you're animating from `opacity: 0` to `opacity: 1`, we know that the current `opacity` in the current frame should be `0.5`. 17 | 18 | ![In renature, we map the mover's position to a value between your from and to states.](../../public/position_change.svg) 19 | 20 | You may be asking yourself, how do we determine the trajectory of the moving object's path? What conditions do we use to say that the `mover` has reached its target destination? To understand this better, read more on the [**Simulate**](./the-lifecycle-of-an-animation.md#simulate) step of `renature`'s animation lifecycle. 21 | -------------------------------------------------------------------------------- /docs/content/core-concepts/vectors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vectors 3 | order: 0 4 | --- 5 | 6 | ## Vectors 7 | 8 | `renature` uses two-dimensional vectors as the core data type for running our physics simulations. [Vectors](https://natureofcode.com/book/chapter-1-vectors/) are mathematical objects that have both magnitude and direction, and are typically represented in two dimensions using a directed line. 9 | 10 | ![Vectors have a magnitude and a direction.](../../public/vector.svg) 11 | 12 | When modeling real-world motion in two dimensions, vectors are particularly useful. We can use vectors to represent where an object is in space (`position`), how fast an object is moving through space (`velocity`), and the rate of change of its speed (`acceleration`). `renature` tracks each of these motion vectors in animation state to determine how an object moves over time. 13 | 14 | We use [ReScript](https://rescript-lang.org/) to model vectors in `renature`, using a data structure called a [tuple](https://rescript-lang.org/docs/manual/latest/tuple). 15 | 16 | ```reason 17 | /* A moving object located at x: 5m, y: 10m in our coordinate system. */ 18 | let position = (5., 10.) 19 | 20 | /* An object moving at -0.5m/s in the x direction and 2m/s in the y direction. */ 21 | let velocity = (-0.5, 2.) 22 | 23 | /* An object decelerating at -0.1 m/s^2 in the x direction and 24 | accelerating at 0.005 m/s^2 in the y direction. */ 25 | let acceleration = (-0.1, 0.005) 26 | ``` 27 | 28 | Forces can be modeled as vectors as well. Forces have a magnitude (how strong the force is) and a direction (the path the force is acting along). 29 | 30 | ```reason 31 | let force = (-5., -2.) 32 | ``` 33 | 34 | With this knowledge of a moving object's motion vectors and an external force vector, we can determine how applying a force onto a moving object will alter that object's `acceleration`, `velocity`, and `position`. To do that, we use the following equations from Newtonian physics. 35 | 36 | ![Force equals mass times acceleration.](../../public/force_equation.svg) 37 | 38 | ![Acceleration equals force divided by mass.](../../public/acceleration_equation.svg) 39 | 40 | ![The velocity of an object is equal to its current velocity plus acceleration times time.](../../public/velocity_equation.svg) 41 | 42 | ![The position of an object is equal to its current position plus velocity times time.](../../public/position_equation.svg) 43 | 44 | With just these laws, we can build a comprehensive model of motion in two dimensions. To learn more, check out Daniel Shiffman's excellent writing on [Vectors](https://natureofcode.com/book/chapter-1-vectors/) or read [the source](https://github.com/FormidableLabs/renature/blob/saturn/src/core/Vector.res) for our vector math. 45 | -------------------------------------------------------------------------------- /docs/content/getting-started/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | order: 1 4 | --- 5 | 6 | # Getting Started 7 | 8 | In this section, we'll cover how to install `renature` and begin writing animations with the library. 9 | 10 | - Check out [Your First Animation](./your-first-animation.md) to get your first animation working. 11 | - Dive into controlling animations with the `Controller` API in [Controlling Animation States](./controlling-animation-states.md). 12 | - Have questions? Check out our [FAQ](./faq.md). 13 | 14 | ## Installation 15 | 16 | To begin using `renature`, install it from the package manager of your choice: 17 | 18 | ```bash 19 | npm install --save renature 20 | # or 21 | yarn add renature 22 | ``` 23 | 24 | Also make sure you install its `peerDependencies`, `react` and `react-dom`, if you haven't already. You'll need to use a hooks compatible version of `react` and `react-dom`, i.e. `>=16.8.0`. 25 | 26 | ```bash 27 | npm install --save react react-dom 28 | # or 29 | yarn add react react-dom 30 | ``` 31 | 32 | That's it! You're now ready to use `renature`. 33 | -------------------------------------------------------------------------------- /docs/content/getting-started/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | order: 4 4 | --- 5 | 6 | ## Frequently Asked Questions 7 | 8 | This page covers some commonly asked questions when getting going with `renature`. Don't see your question here? Open an issue on GitHub and we'll add it! 9 | 10 | ### What is the `props` object returned by the hook? 11 | 12 | The `props` object returned by every hook in `renature` is just a mutable React `ref` object. We attach that `ref` using the object spread operator to your DOM node, which allows `renature` to update that node's `style` attribute during the course of the `requestAnimationFrame` loop. In this way, we can animate UI elements synchronously without re-rendering your entire component on every frame. This is a must to keep your animations performing at 60 frames per second, as tracking animation values in React state would force a re-render for every frame. 13 | 14 | ### When do my animations run? 15 | 16 | By default, animations in `renature` run immediately when your component mounts. This is often expected and desirable behavior – an element appears and starts moving right away. However, you have full control over when and how you want your animations to run. To learn more, read up on `renature`'s [`controller` API](./controlling-animation-states#the-controller-api). 17 | 18 | ### What CSS properties are supported? 19 | 20 | `renature` supports the vast majority of CSS properties, including complex properties like `transform` and `box-shadow`, colors represented in hexadecimal, `rgba`, `hsla`, and CSS colors names, and SVG properties like `stroke-dasharray` and `stroke-dashoffset`. Trying to animate something and it doesn't seem supported? Please open an issue! We'd love to ensure we're supporting the full spectrum of animatable CSS properties! 21 | -------------------------------------------------------------------------------- /docs/content/getting-started/your-first-animation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Your First Animation 3 | order: 1 4 | --- 5 | 6 | ## Your First Animation 7 | 8 | Once you've installed `renature`, you can start off by importing one of the hooks exposed by the library. Let's start off by using the `useFriction` hook. 9 | 10 | ```js live=true 11 | import React from 'react'; 12 | import { useFriction } from 'renature'; 13 | 14 | function Mover() { 15 | const [props] = useFriction({ 16 | from: { 17 | transform: 'translateX(-100px)', 18 | }, 19 | to: { 20 | transform: 'translateX(100px)', 21 | }, 22 | config: { 23 | mu: 0.2, 24 | mass: 20, 25 | initialVelocity: 5, 26 | }, 27 | repeat: Infinity, 28 | }); 29 | 30 | return
; 31 | } 32 | ``` 33 | 34 | `useFriction`, like the others hooks in `renature`, expects a `from` and `to` configuration describing the CSS states you want to animate from and to. These are objects with an arbitrary number of key-value pairs of CSS properties, just like you'd pass to an element's `style` property in React. 35 | 36 | The `config` object represents the parameters that can be used to tweak your physics simulations. You can see the full variety of config options in the [API reference](../api.md#config); these will vary from hook to hook depending on the force that you are using. The best way to see what these parameters do is to play with them in our live code blocks (like the one above 👆). You can read more about `renature`'s physics simulations in the [Core Concepts](../core-concepts.md) section. 37 | 38 | ### Animating Multiple Properties 39 | 40 | With `renature`, you can animate as many CSS properties as you like at once! You can also specify multiple values for complex properties like `transform` and `box-shadow`, and `renature` will interpolate each of them automatically for you ✨. Let's look at an example. 41 | 42 | ```js live=true 43 | import React from 'react'; 44 | import { useFriction } from 'renature'; 45 | 46 | function MoverMultiple() { 47 | const [props] = useFriction({ 48 | from: { 49 | transform: 'scale(0) rotate(0deg)', 50 | background: 'orange', 51 | borderRadius: '0%', 52 | }, 53 | to: { 54 | transform: 'scale(2) rotate(360deg)', 55 | background: 'steelblue', 56 | borderRadius: '50%', 57 | }, 58 | config: { 59 | mu: 0.2, 60 | mass: 20, 61 | initialVelocity: 5, 62 | }, 63 | repeat: Infinity, 64 | }); 65 | 66 | return
; 67 | } 68 | ``` 69 | 70 | In general, you always want to specify a matching `to` value for any property specified in `from`; not doing so will result in the property not being animated. In future versions of `renature` we may provide safe defaults for unspecified `from` and `to` properties. 71 | -------------------------------------------------------------------------------- /docs/formideploy.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Formideploy configuration overrides. 3 | */ 4 | module.exports = { 5 | lander: { 6 | name: 'renature', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formidable-renature-docs", 3 | "version": "5.1.0", 4 | "description": "Documentation site for renature", 5 | "main": "static.config.js", 6 | "license": "MIT", 7 | "bugs": { 8 | "url": "https://github.com/FormidableLabs/renature/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/FormidableLabs/renature.git" 13 | }, 14 | "homepage": "https://github.com/FormidableLabs/renature#readme", 15 | "scripts": { 16 | "start": "react-static start", 17 | "clean": "rimraf dist", 18 | "build": "react-static build", 19 | "serve": "formideploy serve", 20 | "deploy:prod": "formideploy deploy --production", 21 | "deploy:stage": "formideploy deploy --staging", 22 | "prettier-all": "npx prettier --write '{src,static.config,scripts}/**/*.{js,jsx}'", 23 | "preversion": "yarn run lint && yarn run build", 24 | "format": "yarn run lint --fix", 25 | "lint": "eslint 'src/**/*.js' 'static.config.js' 'formideploy.config.js'", 26 | "check-ci": "yarn run lint" 27 | }, 28 | "dependencies": { 29 | "@mdx-js/react": "^1.6.1", 30 | "formidable-oss-badges": "^0.3.4", 31 | "prop-types": "^15.7.2", 32 | "react": "^16.13.1", 33 | "react-copy-to-clipboard": "^5.0.2", 34 | "react-dom": "^16.13.1", 35 | "react-gtm-module": "^2.0.11", 36 | "react-is": "^16.13.1", 37 | "react-live": "^2.2.3", 38 | "react-router-dom": "^5.2.0", 39 | "react-router-ga": "^1.2.3", 40 | "react-static": "^7.4.2", 41 | "react-static-plugin-md-pages": "^0.1.7", 42 | "react-static-plugin-react-router": "^7.4.2", 43 | "react-static-plugin-sitemap": "^7.4.2", 44 | "react-static-plugin-styled-components": "^7.3.0", 45 | "renature": "^0.11.0", 46 | "styled-components": "^5.2.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.12.10", 50 | "@babel/eslint-parser": "^7.12.1", 51 | "chalk": "^4.1.0", 52 | "eslint": "^7.19.0", 53 | "eslint-config-prettier": "^7.2.0", 54 | "eslint-plugin-import": "^2.22.1", 55 | "eslint-plugin-prettier": "^3.3.1", 56 | "eslint-plugin-react": "^7.22.0", 57 | "eslint-plugin-react-hooks": "^4.2.0", 58 | "execa": "^4.0.3", 59 | "formideploy": "^0.3.4", 60 | "prettier": "^2.2.1", 61 | "prism-react-renderer": "^1.1.1", 62 | "react-hot-loader": "^4.13.0", 63 | "rimraf": "^3.0.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ff4081 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/public/favicon/Favicon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/renature/e3fbf3f47108ca508b48ccf142dfc819800db87b/docs/public/favicon/Favicon128.png -------------------------------------------------------------------------------- /docs/public/favicon/Favicon144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/renature/e3fbf3f47108ca508b48ccf142dfc819800db87b/docs/public/favicon/Favicon144.png -------------------------------------------------------------------------------- /docs/public/favicon/Favicon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/renature/e3fbf3f47108ca508b48ccf142dfc819800db87b/docs/public/favicon/Favicon32.png -------------------------------------------------------------------------------- /docs/public/favicon/Favicon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/renature/e3fbf3f47108ca508b48ccf142dfc819800db87b/docs/public/favicon/Favicon48.png -------------------------------------------------------------------------------- /docs/public/from-to.svg: -------------------------------------------------------------------------------- 1 | Opacity 0FromToOpacity 1 -------------------------------------------------------------------------------- /docs/public/gravity_simulation.svg: -------------------------------------------------------------------------------- 1 | MoverAttractorOpacity 0Opacity 1Animation StartsAnimation FinishesMover Accelerates Towards AttractorOpacity Changes at Rate of Position ChangeInterpolationSimulation -------------------------------------------------------------------------------- /docs/public/position_change.svg: -------------------------------------------------------------------------------- 1 | Opacity 0Opacity 1Opacity Changes at Rate of Position Change -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "./favicon/Favicon32.png", 7 | "sizes": "32x32", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./favicon/Favicon48.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./favicon/Favicon128.png", 17 | "sizes": "128x128", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "./favicon/Favicon32.png", 22 | "sizes": "144x144", 23 | "type": "image/png" 24 | } 25 | ], 26 | "theme_color": "#ffffff", 27 | "background_color": "#ffffff", 28 | "display": "standalone" 29 | } 30 | -------------------------------------------------------------------------------- /docs/public/vector.svg: -------------------------------------------------------------------------------- 1 | √5²+2² = √2925v = (5, 2), mag = √29 -------------------------------------------------------------------------------- /docs/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense, useEffect } from 'react'; 2 | import { Root, Routes } from 'react-static'; 3 | import { ThemeProvider } from 'styled-components'; 4 | 5 | import { GlobalStyle } from './styles/global-style'; 6 | import Analytics from './google-analytics'; 7 | import { theme } from './styles/theme'; 8 | import { Loading } from './components/loading'; 9 | import { initGoogleTagManager } from './google-tag-manager'; 10 | 11 | const App = () => { 12 | useEffect(() => { 13 | initGoogleTagManager(); 14 | }, []); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | }> 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /docs/src/assets/anchor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SvgAnchor = (props) => ( 4 | 5 | 10 | 11 | ); 12 | 13 | export default SvgAnchor; 14 | -------------------------------------------------------------------------------- /docs/src/assets/anchor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/src/assets/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/assets/arrow_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/assets/background_renature.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/src/assets/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/src/assets/chevron.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SvgChevron = (props) => ( 4 | 5 | 10 | 11 | ); 12 | 13 | export default SvgChevron; 14 | -------------------------------------------------------------------------------- /docs/src/assets/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/src/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/src/assets/feature-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/src/assets/feature-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/src/assets/header_triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 7 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/src/components/body-copy.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const BodyCopy = styled.p` 5 | font-family: ${(p) => p.theme.fonts.body}; 6 | font-size: 1.5rem; 7 | line-height: 1.5; 8 | color: ${({ color, theme }) => color || theme.colors.textDark}; 9 | text-align: center; 10 | `; 11 | 12 | BodyCopy.propTypes = { 13 | color: PropTypes.string, 14 | }; 15 | -------------------------------------------------------------------------------- /docs/src/components/bounce-animation.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const BounceAnimation = styled.span` 4 | display: block; 5 | transition: all 0.3s ease-out; 6 | transform: ${(props) => 7 | props.bouncing ? 'translateY(-0.5rem)' : 'translateY(0)'}; 8 | `; 9 | -------------------------------------------------------------------------------- /docs/src/components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { theme } from '../styles/theme'; 7 | 8 | export const Button = styled(({ light, isExternal, ...rest }) => { 9 | return isExternal ? ( 10 | 11 | {rest.children} 12 | 13 | ) : ( 14 | 15 | ); 16 | })` 17 | align-self: center; 18 | background: ${({ light }) => 19 | light ? theme.colors.buttonLight : theme.colors.button}; 20 | color: ${({ light }) => 21 | light ? theme.colors.button : theme.colors.buttonLight}; 22 | font-size: 1.5rem; 23 | letter-spacing: 0.01rem; 24 | padding: 1.5rem 2rem; 25 | text-align: center; 26 | text-transform: uppercase; 27 | text-decoration: none; 28 | transition: background 0.4s ease-out; 29 | 30 | &:hover { 31 | background: ${({ light }) => 32 | light ? theme.colors.buttonLightHover : theme.colors.buttonHover}; 33 | } 34 | 35 | &:active { 36 | opacity: 0.6; 37 | } 38 | `; 39 | 40 | Button.propTypes = { 41 | to: PropTypes.string.isRequired, 42 | light: PropTypes.bool, 43 | noMargin: PropTypes.bool, 44 | }; 45 | -------------------------------------------------------------------------------- /docs/src/components/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import Hero from '../screens/home/hero'; 5 | import heroBackground from '../assets/background_renature.svg'; 6 | import headerTriangle from '../assets/header_triangle.svg'; 7 | import logoFormidableWhite from '../assets/logos/logo_formidable_white.svg'; 8 | 9 | const Container = styled.header` 10 | background-image: url(${heroBackground}); 11 | background-size: cover; 12 | color: ${(p) => p.theme.colors.textLight}; 13 | padding-bottom: 8rem; 14 | `; 15 | 16 | const Triangle = styled.img` 17 | position: absolute; 18 | display: block; 19 | left: 0; 20 | top: 0; 21 | width: 14rem; 22 | 23 | @media ${(p) => p.theme.media.sm} { 24 | width: 22rem; 25 | } 26 | 27 | @media ${(p) => p.theme.media.md} { 28 | width: 28rem; 29 | } 30 | `; 31 | 32 | const HeaderContainer = styled.a` 33 | display: flex; 34 | flex-direction: column; 35 | position: absolute; 36 | left: 2rem; 37 | top: 1.5rem; 38 | font-size: 0.8rem; 39 | color: ${(p) => p.theme.colors.textLight}; 40 | text-decoration: none; 41 | 42 | @media ${(p) => p.theme.media.sm} { 43 | left: 3.5rem; 44 | top: 2rem; 45 | font-size: 1.2rem; 46 | } 47 | 48 | @media ${(p) => p.theme.media.md} { 49 | left: 4rem; 50 | top: 3rem; 51 | } 52 | `; 53 | 54 | const HeaderText = styled.p` 55 | text-transform: uppercase; 56 | margin: 0; 57 | line-height: 1.5; 58 | letter-spacing: 0.086rem; 59 | max-width: 10rem; 60 | `; 61 | 62 | const HeaderLogo = styled.img` 63 | width: 3rem; 64 | margin-top: 1rem; 65 | 66 | @media ${(p) => p.theme.media.sm} { 67 | width: 4rem; 68 | } 69 | 70 | @media ${(p) => p.theme.media.md} { 71 | width: 6rem; 72 | } 73 | `; 74 | 75 | export const Header = () => ( 76 | 77 | 78 | 84 | Another oss project by 85 | 86 | 87 | 88 | 89 | ); 90 | -------------------------------------------------------------------------------- /docs/src/components/loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | import Docs from '../screens/docs'; 5 | 6 | const Container = styled.div` 7 | height: 100vh; 8 | width: 100%; 9 | `; 10 | 11 | const Loader = styled.div` 12 | position: relative; 13 | margin: 0 auto; 14 | width: ${(p) => p.theme.spacing.xl}; 15 | top: calc(50% - ${(p) => p.theme.spacing.xl}); 16 | 17 | &:before { 18 | content: ''; 19 | display: block; 20 | padding-top: 100%; 21 | } 22 | `; 23 | 24 | const rotate = keyframes` 25 | 100% { 26 | transform: rotate(360deg); 27 | } 28 | `; 29 | 30 | const dash = keyframes` 31 | 0% { 32 | stroke-dasharray: 1, 200; 33 | stroke-dashoffset: 0; 34 | } 35 | 50% { 36 | stroke-dasharray: 89, 200; 37 | stroke-dashoffset: -35px; 38 | } 39 | 100% { 40 | stroke-dasharray: 89, 200; 41 | stroke-dashoffset: -124px; 42 | } 43 | `; 44 | 45 | const Svg = styled.svg` 46 | animation: ${rotate} 2s linear infinite; 47 | height: 100%; 48 | transform-origin: center center; 49 | width: 100%; 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | left: 0; 54 | right: 0; 55 | margin: auto; 56 | `; 57 | 58 | const Circle = styled.circle` 59 | stroke: ${(p) => p.theme.colors.accent}; 60 | stroke-dasharray: 1, 200; 61 | stroke-dashoffset: 0; 62 | animation: ${dash} 1.5s ease-in-out infinite; 63 | stroke-linecap: round; 64 | `; 65 | 66 | export const Loading = () => ( 67 | 68 | 69 | 70 | 71 | 80 | 81 | 82 | 83 | 84 | ); 85 | -------------------------------------------------------------------------------- /docs/src/components/scroll-to-top.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { useMarkdownPage } from 'react-static-plugin-md-pages'; 4 | 5 | const parsePathname = (pathname) => { 6 | const match = pathname && pathname.match(/#[a-z|-]+/); 7 | return match && match[1]; 8 | }; 9 | 10 | export const ScrollToTop = () => { 11 | const inputRef = useRef(null); 12 | const location = useLocation(); 13 | const md = useMarkdownPage(); 14 | 15 | const hash = location.hash || parsePathname(location.pathname); 16 | 17 | useEffect(() => { 18 | if (hash && md) { 19 | inputRef.current.click(); 20 | } else { 21 | window.scrollTo(0, 0); 22 | } 23 | }, [hash, md]); 24 | 25 | return ; 26 | }; 27 | -------------------------------------------------------------------------------- /docs/src/components/secondary-title.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const SecondaryTitle = styled.h2` 4 | color: ${({ theme }) => theme.colors.textDark}; 5 | font-size: 2rem; 6 | line-height: 2.4rem; 7 | text-align: center; 8 | `; 9 | -------------------------------------------------------------------------------- /docs/src/components/section-stack.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { stack } from '../styles/mixins'; 4 | 5 | export const SectionStack = styled.div` 6 | ${stack(3, 5)}; 7 | `; 8 | -------------------------------------------------------------------------------- /docs/src/components/section-title.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const SectionTitle = styled.h2` 5 | color: ${({ color, theme }) => color || theme.colors.textDark}; 6 | font-size: 3rem; 7 | flex: auto; 8 | line-height: 1.3; 9 | width: 100%; 10 | text-align: center; 11 | 12 | @media ${(p) => p.theme.media.sm} { 13 | font-size: 4rem; 14 | } 15 | 16 | @media ${(p) => p.theme.media.md} { 17 | font-size: 4.5rem; 18 | } 19 | `; 20 | 21 | SectionTitle.propTypes = { 22 | color: PropTypes.string, 23 | }; 24 | -------------------------------------------------------------------------------- /docs/src/components/toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const StyledToggle = styled.div` 6 | width: 5.5rem; 7 | 8 | input { 9 | opacity: 0; 10 | position: absolute; 11 | } 12 | 13 | input + label { 14 | position: relative; 15 | display: inline-block; 16 | user-select: none; 17 | height: 3rem; 18 | width: 5rem; 19 | border: 1px solid #e4e4e4; 20 | border-radius: 6rem; 21 | transition: 0.4s ease; 22 | cursor: pointer; 23 | } 24 | 25 | input + label::before { 26 | content: ''; 27 | position: absolute; 28 | display: block; 29 | height: 3rem; 30 | width: 5rem; 31 | top: 0; 32 | left: 0; 33 | border-radius: 3rem; 34 | transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1); 35 | } 36 | 37 | input + label::after { 38 | content: ''; 39 | position: absolute; 40 | display: block; 41 | background: ${(p) => p.theme.colors.accent}; 42 | height: 2.8rem; 43 | width: 2.8rem; 44 | top: 0.1rem; 45 | left: 0; 46 | border-radius: 6rem; 47 | box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 48 | 0 4px 0px 0 hsla(0, 0%, 0%, 0.04), 0 4px 9px hsla(0, 0%, 0%, 0.13), 49 | 0 3px 3px hsla(0, 0%, 0%, 0.05); 50 | transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1); 51 | } 52 | 53 | input:checked + label::before { 54 | background: ${(p) => p.theme.colors.buttonLightHover}; 55 | transition: width 0.2s cubic-bezier(0, 0, 0, 0.1); 56 | } 57 | 58 | input:checked + label::after { 59 | left: 2.4rem; 60 | } 61 | `; 62 | 63 | export const Toggle = ({ onChange, checked }) => ( 64 | 65 | 72 | 74 | ); 75 | 76 | Toggle.propTypes = { 77 | onChange: PropTypes.func.isRequired, 78 | checked: PropTypes.bool.isRequired, 79 | }; 80 | -------------------------------------------------------------------------------- /docs/src/components/wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const Wrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | flex-wrap: wrap; 8 | justify-content: space-between; 9 | width: 100%; 10 | margin: ${({ noMargin }) => (noMargin ? '0' : 'auto')}; 11 | padding: ${({ noPadding }) => (noPadding ? '0' : '4rem')}; 12 | background: ${({ theme, background }) => background || theme.colors.bgLight}; 13 | text-align: center; 14 | 15 | @media ${(p) => p.theme.media.sm} { 16 | padding: ${({ noPadding }) => (noPadding ? '0' : '8rem')}; 17 | } 18 | `; 19 | 20 | Wrapper.propTypes = { 21 | noMargin: PropTypes.bool, 22 | noPadding: PropTypes.bool, 23 | background: PropTypes.string, 24 | }; 25 | 26 | Wrapper.defaultProps = { 27 | noMargin: false, 28 | noPadding: false, 29 | }; 30 | -------------------------------------------------------------------------------- /docs/src/constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Renature', 3 | githubIssues: 'https://www.github.com/FormidableLabs/renature/issues', 4 | github: 'https://www.github.com/FormidableLabs/renature', 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/google-analytics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | let Analytics = {}; 5 | 6 | if (typeof document !== 'undefined') { 7 | Analytics = require('react-router-ga').default; 8 | } else { 9 | Analytics = React.Fragment; 10 | } 11 | 12 | const GoogleAnalytics = ({ children, ...rest }) => { 13 | if (typeof document !== 'undefined') { 14 | // fragment doesn't like it when you try to give it attributes 15 | return {children}; 16 | } 17 | return {children}; 18 | }; 19 | 20 | GoogleAnalytics.propTypes = { 21 | children: PropTypes.element, 22 | }; 23 | 24 | export default GoogleAnalytics; 25 | -------------------------------------------------------------------------------- /docs/src/google-tag-manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Google Tag Manager 3 | */ 4 | const TagManager = require('react-gtm-module'); 5 | 6 | export const initGoogleTagManager = () => { 7 | if (typeof document === 'undefined') { 8 | return; 9 | } else { 10 | return TagManager.initialize({ gtmId: 'GTM-MD32945' }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /docs/src/html.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const CustomDocument = ({ Html, Head, Body, children }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 43 | renature Documentation 44 | 45 | 46 |
{children}
47 | 48 | 49 | ); 50 | }; 51 | 52 | CustomDocument.propTypes = { 53 | Body: PropTypes.func.isRequired, 54 | Head: PropTypes.func.isRequired, 55 | Html: PropTypes.func.isRequired, 56 | children: PropTypes.node.isRequired, 57 | }; 58 | 59 | export default CustomDocument; 60 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, hydrate } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | 5 | import App from './app'; 6 | 7 | export default App; 8 | 9 | if (typeof document !== 'undefined') { 10 | const renderMethod = module.hot ? render : hydrate; 11 | const mount = (Component) => { 12 | renderMethod( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | }; 19 | 20 | mount(App); 21 | if (module.hot) { 22 | module.hot.accept('./app', () => mount(require('./app').default)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/screens/404/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useBasepath } from 'react-static'; 3 | import { Link } from 'react-router-dom'; 4 | import styled from 'styled-components'; 5 | 6 | const Container = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | flex: 1; 12 | height: ${(p) => `calc(100vh - ${p.theme.layout.header})`}; 13 | `; 14 | 15 | const Heading = styled.h1` 16 | text-align: center; 17 | `; 18 | 19 | const StyledLink = styled(Link)` 20 | background: ${(p) => p.theme.colors.accent}; 21 | color: ${(p) => p.theme.colors.textLight}; 22 | text-decoration: none; 23 | text-transform: uppercase; 24 | font-size: 2rem; 25 | font-size: 1.5rem; 26 | letter-spacing: 0.01rem; 27 | padding: 1.5rem 2rem; 28 | transition: color 0.4s ease-out; 29 | 30 | &:hover { 31 | color: ${(p) => p.theme.colors.buttonLightHover}; 32 | } 33 | `; 34 | 35 | const NotFound = () => { 36 | const basepath = useBasepath() || ''; 37 | const homepage = basepath ? `/${basepath}/` : '/'; 38 | 39 | return ( 40 | 41 | 🔭 42 | It appears you've wandered astray. 43 | Head Home 44 | 45 | ); 46 | }; 47 | 48 | export default NotFound; 49 | -------------------------------------------------------------------------------- /docs/src/screens/404/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Docs from '../docs'; 4 | 5 | import NotFoundPage from './404'; 6 | 7 | const NotFound = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /docs/src/screens/docs/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Link } from 'react-router-dom'; 4 | import { useBasepath } from 'react-static'; 5 | 6 | import formidableLogo from '../../assets/logos/logo_formidable.svg'; 7 | 8 | const Fixed = styled.header` 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | right: 0; 13 | width: 100%; 14 | z-index: 1; 15 | box-sizing: border-box; 16 | height: ${(p) => p.theme.layout.header}; 17 | background: ${(p) => p.theme.colors.bg}; 18 | padding: 0 ${(p) => p.theme.spacing.md}; 19 | box-shadow: ${(p) => p.theme.shadows.header}; 20 | `; 21 | 22 | const Wrapper = styled.div` 23 | width: 100%; 24 | height: 100%; 25 | max-width: ${(p) => p.theme.layout.page}; 26 | margin: 0 auto; 27 | padding-top: 2px; 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | `; 32 | 33 | const BlockLink = styled.a` 34 | display: flex; 35 | color: inherit; 36 | text-decoration: none; 37 | `; 38 | 39 | const ProjectWording = styled(Link)` 40 | display: flex; 41 | text-decoration: none; 42 | font-family: ${(p) => p.theme.fonts.code}; 43 | color: ${(p) => p.theme.colors.accent}; 44 | margin-left: 0.6ch; 45 | font-size: 1.9rem; 46 | `; 47 | 48 | const FormidableLogo = styled.img.attrs(() => ({ 49 | src: formidableLogo, 50 | }))` 51 | height: 2.8rem; 52 | position: relative; 53 | top: -0.1rem; 54 | `; 55 | 56 | const Header = () => { 57 | const basepath = useBasepath() || ''; 58 | const homepage = basepath ? `/${basepath}/` : '/'; 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | renature 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default Header; 73 | -------------------------------------------------------------------------------- /docs/src/screens/docs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import Sidebar from '../../components/sidebar'; 6 | import burger from '../../assets/burger.svg'; 7 | import closeButton from '../../assets/close.svg'; 8 | import { center } from '../../styles/mixins'; 9 | 10 | import Article from './article'; 11 | import Header from './header'; 12 | 13 | const Container = styled.div` 14 | ${center} 15 | 16 | position: relative; 17 | display: flex; 18 | width: 100%; 19 | margin-top: ${(p) => p.theme.layout.header}; 20 | background: ${(p) => p.theme.colors.textLight}; 21 | `; 22 | 23 | export const OpenCloseSidebar = styled.img.attrs((props) => ({ 24 | src: props.sidebarOpen ? closeButton : burger, 25 | }))` 26 | cursor: pointer; 27 | display: block; 28 | margin: ${(p) => p.theme.spacing.sm} ${(p) => p.theme.spacing.md}; 29 | position: fixed; 30 | right: 0; 31 | top: 0; 32 | z-index: 1; 33 | 34 | @media ${(p) => p.theme.media.sm} { 35 | display: none; 36 | } 37 | `; 38 | 39 | const Docs = ({ isLoading, children }) => { 40 | const [sidebarOpen, setSidebarOpen] = React.useState(false); 41 | 42 | const toggleSidebar = () => { 43 | setSidebarOpen((prevOpen) => !prevOpen); 44 | }; 45 | 46 | const closeSidebar = () => { 47 | setSidebarOpen(false); 48 | }; 49 | 50 | return ( 51 | <> 52 |
53 | 54 | 55 | {isLoading ? ( 56 | children 57 | ) : ( 58 | <> 59 | 60 |
{children}
61 | 62 | )} 63 |
64 | 65 | ); 66 | }; 67 | 68 | Docs.propTypes = { 69 | isLoading: PropTypes.bool, 70 | children: PropTypes.node.isRequired, 71 | }; 72 | 73 | export default Docs; 74 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/gallery-preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import { LiveProvider, LivePreview } from 'react-live'; 5 | 6 | import { scope } from '../../utils/live-preview'; 7 | import Arrow from '../../assets/arrow.svg'; 8 | 9 | const StyledCard = styled.div` 10 | display: flex; 11 | flex-direction: column; 12 | border: 0.1rem solid ${(p) => p.theme.colors.accent}; 13 | position: relative; 14 | background-position: 0 0; 15 | background-size: 0.5rem 0.5rem; 16 | background-image: radial-gradient( 17 | ${(p) => p.theme.colors.accent} 25%, 18 | transparent 25% 19 | ); 20 | `; 21 | 22 | const StyledPreview = styled(LivePreview)` 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | height: 15rem; 27 | background: ${(p) => p.theme.colors.textLight}; 28 | `; 29 | 30 | const StyledPreviewTitle = styled.a` 31 | display: flex; 32 | align-items: center; 33 | font-size: ${(p) => p.theme.fontSizes.small}; 34 | margin: 1.5rem auto; 35 | padding: 0.5rem; 36 | background: ${(p) => p.theme.colors.textLight}; 37 | text-decoration: none; 38 | color: ${(p) => p.theme.colors.accent}; 39 | 40 | @media ${(p) => p.theme.media.sm} { 41 | font-size: ${(p) => p.theme.fontSizes.bodySmall}; 42 | } 43 | 44 | @media ${(p) => p.theme.media.md} { 45 | font-size: ${(p) => p.theme.fontSizes.body}; 46 | } 47 | 48 | &::after { 49 | content: ''; 50 | background: url(${Arrow}); 51 | height: 1.6rem; 52 | width: 1.6rem; 53 | margin-left: 0.5rem; 54 | } 55 | `; 56 | 57 | const GalleryPreview = ({ title, code, demoLink }) => ( 58 | 59 | 60 | 61 | 62 | 67 | {title} 68 | 69 | 70 | ); 71 | 72 | GalleryPreview.propTypes = { 73 | title: PropTypes.string.isRequired, 74 | code: PropTypes.string.isRequired, 75 | demoLink: PropTypes.string.isRequired, 76 | }; 77 | 78 | export default GalleryPreview; 79 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/basic-transform.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function BasicTransform() { 3 | const [props] = useGravity({ 4 | from: { transform: 'translateX(-100px)' }, 5 | to: { transform: 'translateX(100px)' }, 6 | config: { 7 | moverMass: 10000, 8 | attractorMass: 1000000000000, 9 | r: 10, 10 | }, 11 | repeat: Infinity 12 | }); 13 | 14 | return
; 15 | } 16 | `; 17 | 18 | export const basicTransform = { 19 | title: 'Basic Transform', 20 | slug: 'basic-transform/', 21 | code, 22 | demoLink: 'https://codesandbox.io/s/renature-basic-transform-21u2w', 23 | }; 24 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/box-shadow.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function BoxShadow() { 3 | const [props] = useFriction({ 4 | from: { 5 | boxShadow: "-1rem 1rem teal, 1rem -1rem #f2cf63", 6 | transform: "translate(-2rem, -2rem)" 7 | }, 8 | to: { 9 | boxShadow: "1rem -1rem orange, -1rem 1rem #f25050", 10 | transform: "translate(2rem, 2rem)" 11 | }, 12 | config: { 13 | mu: 0.4, 14 | mass: 25, 15 | initialVelocity: 10 16 | }, 17 | repeat: Infinity 18 | }); 19 | 20 | return
; 21 | } 22 | `; 23 | 24 | export const boxShadow = { 25 | title: 'Box Shadow', 26 | slug: 'box-shadow/', 27 | code, 28 | demoLink: 'https://codesandbox.io/s/renature-box-shadow-1ytkc', 29 | }; 30 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/controlled-animations.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function ControlledMover() { 3 | const [props, controller] = useFriction({ 4 | from: { 5 | transform: 'scale(1) skew(0deg, 0deg)', 6 | }, 7 | to: { 8 | transform: 'scale(0) skew(90deg, 90deg)', 9 | }, 10 | config: { 11 | mu: 0.1, 12 | mass: 50, 13 | initialVelocity: 1, 14 | }, 15 | pause: true, 16 | }); 17 | 18 | return ( 19 |
20 | 26 |
27 |
28 | ); 29 | } 30 | `; 31 | 32 | export const controlledAnimations = { 33 | title: 'Controlled Animations', 34 | slug: 'controlled-animations/', 35 | code, 36 | demoLink: 'https://codesandbox.io/s/renature-controlled-animations-4rwe3', 37 | }; 38 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/grouped-animations.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function FrictionGroup() { 3 | const [nodes] = useFrictionGroup(4, i => ({ 4 | from: { 5 | transform: 'translateX(0px)', 6 | fill: '#FFCE24', 7 | }, 8 | to: { 9 | transform: 'translateX(20px)', 10 | fill: '#FA24FF', 11 | }, 12 | config: { 13 | mu: 0.5, 14 | mass: 200, 15 | initialVelocity: 5, 16 | }, 17 | delay: i * 500, 18 | repeat: Infinity, 19 | })); 20 | 21 | return ( 22 | 23 | {nodes.map((props, i) => { 24 | const xOffset = i * 50; 25 | const points = 26 | xOffset.toString() + 27 | ",20 " + 28 | (xOffset + 50).toString() + 29 | ",50 " + 30 | xOffset.toString() + 31 | ",80"; 32 | 33 | return ( 34 | 35 | ); 36 | })} 37 | 38 | ); 39 | } 40 | `; 41 | 42 | export const groupedAnimations = { 43 | title: 'Grouped Animations', 44 | slug: 'grouped-animations/', 45 | code, 46 | demoLink: 'https://codesandbox.io/s/renature-grouped-animations-drimw', 47 | }; 48 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/index.js: -------------------------------------------------------------------------------- 1 | import { basicTransform } from './basic-transform'; 2 | import { multipleProperties } from './multiple-properties'; 3 | import { controlledAnimations } from './controlled-animations'; 4 | import { pathTracing } from './path-tracing'; 5 | import { boxShadow } from './box-shadow'; 6 | import { orbit } from './orbit'; 7 | import { groupedAnimations } from './grouped-animations'; 8 | import { set } from './set'; 9 | import { repeatType } from './repeat-type'; 10 | 11 | export const samples = [ 12 | basicTransform, 13 | multipleProperties, 14 | controlledAnimations, 15 | pathTracing, 16 | boxShadow, 17 | orbit, 18 | groupedAnimations, 19 | set, 20 | repeatType, 21 | ]; 22 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/multiple-properties.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function Mover() { 3 | const [props] = useFriction({ 4 | from: { 5 | transform: 'scale(0) rotate(0deg)', 6 | background: 'orange', 7 | borderRadius: '0%', 8 | }, 9 | to: { 10 | transform: 'scale(2) rotate(360deg)', 11 | background: 'steelblue', 12 | borderRadius: '50%', 13 | }, 14 | config: { 15 | mu: 0.2, 16 | mass: 20, 17 | initialVelocity: 5, 18 | }, 19 | repeat: Infinity, 20 | }); 21 | 22 | return
; 23 | } 24 | `; 25 | 26 | export const multipleProperties = { 27 | title: 'Multiple CSS Properties', 28 | slug: 'multiple-css-properties/', 29 | code, 30 | demoLink: 'https://codesandbox.io/s/renature-multiple-css-properties-h3oep', 31 | }; 32 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/orbit.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function Orbit() { 3 | const config = { 4 | attractorMass: 1000000000000, 5 | moverMass: 10000, 6 | attractorPosition: [0, 0], 7 | threshold: { 8 | min: 20, 9 | max: 100, 10 | }, 11 | timeScale: 100, 12 | }; 13 | 14 | const [planetOne] = useGravity2D({ 15 | config: { 16 | ...config, 17 | initialMoverPosition: [0, -50], 18 | initialMoverVelocity: [1, 0], 19 | }, 20 | }); 21 | 22 | const [planetTwo] = useGravity2D({ 23 | config: { 24 | ...config, 25 | initialMoverPosition: [0, 50], 26 | initialMoverVelocity: [-1, 0], 27 | }, 28 | }); 29 | 30 | return ( 31 |
32 |
35 |
39 |
43 |
44 | ); 45 | } 46 | `; 47 | 48 | export const orbit = { 49 | title: 'Orbit', 50 | slug: 'orbit/', 51 | code, 52 | demoLink: 'https://codesandbox.io/s/renature-orbit-7w6z0', 53 | }; 54 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/repeat-type.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function Mover() { 3 | const [nodes] = useFrictionGroup(2, (i) => ({ 4 | from: { 5 | transform: 'translateX(-100px)', 6 | }, 7 | to: { 8 | transform: 'translateX(100px)', 9 | }, 10 | config: { 11 | mu: 0.2, 12 | mass: 20, 13 | initialVelocity: 5, 14 | }, 15 | repeat: Infinity, 16 | repeatType: i === 0 ? 'mirror' : 'loop' 17 | })); 18 | 19 | return ( 20 |
21 | {nodes.map((props, i) =>
)} 22 |
23 | ); 24 | } 25 | `; 26 | 27 | export const repeatType = { 28 | title: 'Repeat Type', 29 | slug: 'repeat-type/', 30 | code, 31 | demoLink: 'https://codesandbox.io/s/renature-repeat-type-wufv0', 32 | }; 33 | -------------------------------------------------------------------------------- /docs/src/screens/gallery/samples/set.js: -------------------------------------------------------------------------------- 1 | const code = ` 2 | function ControllerSet() { 3 | const [props, controller] = useFriction({ 4 | from: { 5 | opacity: 0, 6 | }, 7 | to: { 8 | opacity: 1, 9 | }, 10 | config: { 11 | mu: 0.5, 12 | mass: 300, 13 | initialVelocity: 10, 14 | }, 15 | }); 16 | 17 | React.useEffect(() => { 18 | const intervalId = setInterval(() => { 19 | controller.set({ 20 | transform: "rotate(" + Math.random() * 360 + "deg)" + 21 | " scale(" + Math.random() * 2 + ")", 22 | opacity: Math.random(), 23 | }); 24 | }, 2000); 25 | 26 | return () => { 27 | clearInterval(intervalId); 28 | }; 29 | }, [controller]); 30 | 31 | return
; 32 | } 33 | `; 34 | 35 | export const set = { 36 | title: 'controller.set', 37 | slug: 'controller-set/', 38 | code, 39 | demoLink: 'https://codesandbox.io/s/renature-controllerset-6dyj1', 40 | }; 41 | -------------------------------------------------------------------------------- /docs/src/screens/home/_content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const content = { 4 | features: [ 5 | { 6 | title: 'Declarative React hooks for animating with ease', 7 | description: 8 | 'Tweak your physics parameters, set from and to values for your CSS properties, and let renature do the rest.', 9 | icon: require('../../assets/feature-1.svg'), 10 | }, 11 | { 12 | title: 'Gravity, Friction, Fluid Resistance, and more', 13 | description: 14 | 'Renature explores forces that other physics-based animation libraries typically leave out, giving your animations unique feeling and intuitive motion.', 15 | icon: require('../../assets/feature-2.svg'), 16 | }, 17 | { 18 | title: 'An animation library for physics nerds', 19 | description: 20 | 'Renature emphasizes mathematical precision and correctness, all backed by the type safety and speed of ReScript.', 21 | icon: require('../../assets/feature-3.svg'), 22 | }, 23 | ], 24 | getStarted: { 25 | description: ( 26 | <> 27 | Renature comes equipped with a lightweight set of production ready React 28 | hooks. 29 |
30 | Dig into the documentation to start animating! 31 | 32 | ), 33 | link: '/docs', 34 | }, 35 | oss: { 36 | cards: [ 37 | { 38 | title: 'Victory', 39 | description: 40 | 'An ecosystem of modular data visualization components for React. Friendly and flexible.', 41 | link: 'https://formidable.com/open-source/victory', 42 | featured: true, 43 | }, 44 | { 45 | title: 'urql', 46 | description: 47 | 'Universal React Query Library is a blazing-fast GraphQL client, exposed as a set of ReactJS components.', 48 | link: 'https://formidable.com/open-source/urql/', 49 | featured: true, 50 | }, 51 | { 52 | title: 'Spectacle', 53 | description: 54 | 'A React.js based library for creating sleek presentations using JSX syntax that gives you the ability to live demo your code.', 55 | link: 'https://formidable.com/open-source/spectacle/', 56 | featured: true, 57 | }, 58 | { 59 | title: 'Runpkg', 60 | description: 61 | 'The online package explorer. Runpkg turns any npm package into an interactive and informative browsing experience.', 62 | link: 'https://runpkg.com', 63 | abbreviation: 'Rp', 64 | color: '#80EAC7', 65 | }, 66 | ], 67 | link: 'https://formidable.com/open-source/', 68 | }, 69 | }; 70 | 71 | export default content; 72 | -------------------------------------------------------------------------------- /docs/src/screens/home/features.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import { BodyCopy } from '../../components/body-copy'; 6 | import { SecondaryTitle } from '../../components/secondary-title'; 7 | import { SectionTitle } from '../../components/section-title'; 8 | import { Wrapper } from '../../components/wrapper'; 9 | import { theme } from '../../styles/theme'; 10 | import { stack, center } from '../../styles/mixins'; 11 | import { SectionStack } from '../../components/section-stack'; 12 | 13 | const FeaturesContainer = styled.div` 14 | ${center}; 15 | 16 | display: grid; 17 | grid-template-columns: 1fr; 18 | grid-gap: 3rem; 19 | 20 | @media ${(p) => p.theme.media.sm} { 21 | grid-template-columns: repeat(3, 1fr); 22 | } 23 | 24 | @media ${(p) => p.theme.media.md} { 25 | grid-gap: 5rem; 26 | } 27 | `; 28 | 29 | const FeatureCard = styled.div` 30 | flex: 1; 31 | display: flex; 32 | flex-direction: column; 33 | align-items: center; 34 | 35 | ${stack(2)}; 36 | `; 37 | 38 | const FeatureInfo = styled.div` 39 | max-width: 30rem; 40 | `; 41 | 42 | const Features = ({ features }) => ( 43 | 44 | 45 | Features 46 | 47 | {features.map((feature) => { 48 | return ( 49 | 50 | {feature.title} 51 | 52 | {feature.title} 53 | {feature.description} 54 | 55 | 56 | ); 57 | })} 58 | 59 | 60 | 61 | ); 62 | 63 | Features.propTypes = { 64 | features: PropTypes.arrayOf( 65 | PropTypes.shape({ 66 | title: PropTypes.string.isRequired, 67 | icon: PropTypes.string.isRequired, 68 | description: PropTypes.string.isRequired, 69 | }).isRequired 70 | ).isRequired, 71 | }; 72 | 73 | export default Features; 74 | -------------------------------------------------------------------------------- /docs/src/screens/home/get-started.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { BodyCopy } from '../../components/body-copy'; 5 | import { Button } from '../../components/button'; 6 | import { SectionTitle } from '../../components/section-title'; 7 | import { SectionStack } from '../../components/section-stack'; 8 | import { Wrapper } from '../../components/wrapper'; 9 | import { theme } from '../../styles/theme'; 10 | 11 | const GetStarted = ({ getStarted }) => ( 12 | 13 | 14 | Get Started 15 | {getStarted.description} 16 | 17 | 18 | 19 | ); 20 | 21 | GetStarted.propTypes = { 22 | getStarted: PropTypes.shape({ 23 | description: PropTypes.node.isRequired, 24 | link: PropTypes.string.isRequired, 25 | }).isRequired, 26 | }; 27 | 28 | export default GetStarted; 29 | -------------------------------------------------------------------------------- /docs/src/screens/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { usePrefetch } from 'react-static'; 4 | import { useMarkdownTree } from 'react-static-plugin-md-pages'; 5 | 6 | import { Header } from '../../components/header'; 7 | import { Footer } from '../../components/footer'; 8 | import { ScrollToTop } from '../../components/scroll-to-top'; 9 | 10 | import Features from './features'; 11 | import GetStarted from './get-started'; 12 | import MoreOSS from './more-oss'; 13 | import Preview from './preview'; 14 | import content from './_content'; 15 | 16 | const Container = styled.div` 17 | width: 100%; 18 | `; 19 | 20 | const Home = () => { 21 | const ref = usePrefetch('docs'); 22 | useMarkdownTree(); 23 | 24 | return ( 25 | <> 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 |