├── .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 |

3 |
4 |
5 |
6 |
7 |
8 | A physics-based animation library for React inspired by the natural world.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | 
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 | 
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 | 
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 | 
37 |
38 | 
39 |
40 | 
41 |
42 | 
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 |
--------------------------------------------------------------------------------
/docs/public/gravity_simulation.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/public/position_change.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
11 | );
12 |
13 | export default SvgAnchor;
14 |
--------------------------------------------------------------------------------
/docs/src/assets/anchor.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/src/assets/arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/assets/arrow_left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/assets/background_renature.svg:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/docs/src/assets/burger.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/src/assets/chevron.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SvgChevron = (props) => (
4 |
11 | );
12 |
13 | export default SvgChevron;
14 |
--------------------------------------------------------------------------------
/docs/src/assets/chevron.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/src/assets/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/assets/feature-2.svg:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/docs/src/assets/feature-3.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/docs/src/assets/header_triangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
73 |
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 |
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 |
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 |
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 |
34 |
35 | >
36 | );
37 | };
38 |
39 | export default Home;
40 |
--------------------------------------------------------------------------------
/docs/src/screens/home/npm-copy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CopyToClipboard } from 'react-copy-to-clipboard';
4 | import styled from 'styled-components';
5 |
6 | import { BounceAnimation } from '../../components/bounce-animation';
7 |
8 | const HeroNPMWrapper = styled.div`
9 | display: flex;
10 | flex-direction: row;
11 | align-items: center;
12 | flex-basis: 65%;
13 | `;
14 |
15 | const HeroNPMCopy = styled.p`
16 | background-color: #d5d5d5;
17 | height: 4rem;
18 | color: ${(p) => p.theme.colors.button};
19 | text-align: left;
20 | padding: 0 1.5rem;
21 | line-height: 4rem;
22 | font-size: 1.4rem;
23 | letter-spacing: 0.02rem;
24 | margin: 0;
25 | flex: 1;
26 | `;
27 |
28 | const HeroNPMButton = styled.button`
29 | flex: 0 1 8rem;
30 | height: 4rem;
31 | margin: 0;
32 | padding: 0;
33 | background: ${(p) => p.theme.colors.textLight};
34 | transition: background 0.3s ease-out;
35 | font-size: 1.4rem;
36 | letter-spacing: 0.01rem;
37 | color: ${(p) => p.theme.colors.button};
38 | border: 0;
39 | text-transform: uppercase;
40 | cursor: copy;
41 |
42 | &:hover {
43 | background: ${(p) => p.theme.colors.buttonLightHover};
44 | }
45 | `;
46 |
47 | const NpmCopy = ({ text }) => {
48 | const [animating, setAnimating] = React.useState(false);
49 | const [copied, setCopied] = React.useState(false);
50 |
51 | const handleCopy = React.useCallback((ev) => {
52 | ev.preventDefault();
53 | setAnimating(true);
54 | setCopied(true);
55 |
56 | setTimeout(() => {
57 | setAnimating(false);
58 | }, 200);
59 |
60 | setTimeout(() => {
61 | setCopied(false);
62 | }, 2000);
63 | }, []);
64 |
65 | return (
66 |
67 |
68 | {text}
69 |
70 |
71 | {copied ? 'Copied' : 'Copy'}
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | NpmCopy.propTypes = {
80 | text: PropTypes.string.isRequired,
81 | };
82 |
83 | export default NpmCopy;
84 |
--------------------------------------------------------------------------------
/docs/src/styles/mixins.js:
--------------------------------------------------------------------------------
1 | import { css } from 'styled-components';
2 |
3 | export function stack(spacingMobile, spacingTablet = spacingMobile) {
4 | return css`
5 | display: flex;
6 | flex-direction: column;
7 |
8 | > * {
9 | margin-top: 0;
10 | margin-bottom: 0;
11 | }
12 |
13 | > * + * {
14 | margin-top: ${spacingMobile}rem;
15 | }
16 |
17 | @media ${(p) => p.theme.media.sm} {
18 | > * + * {
19 | margin-top: ${spacingTablet}rem;
20 | }
21 | }
22 | `;
23 | }
24 |
25 | export function stackHorizontal(spacingMobile, spacingTablet = spacingMobile) {
26 | return css`
27 | display: flex;
28 | flex-direction: row;
29 |
30 | > * {
31 | margin-left: 0;
32 | margin-right: 0;
33 | }
34 |
35 | > * + * {
36 | margin-left: ${spacingMobile}rem;
37 | }
38 |
39 | @media ${(p) => p.theme.media.sm} {
40 | > * + * {
41 | margin-left: ${spacingTablet}rem;
42 | }
43 | }
44 | `;
45 | }
46 |
47 | export const center = css`
48 | margin-left: auto;
49 | margin-right: auto;
50 | max-width: ${(p) => p.theme.layout.page};
51 | `;
52 |
53 | export function underline({ light } = { light: false }) {
54 | return css`
55 | position: relative;
56 | text-decoration: none;
57 |
58 | &::before {
59 | background-color: ${(p) =>
60 | light ? p.theme.colors.buttonLightHover : p.theme.colors.accent};
61 | bottom: 0;
62 | content: '';
63 | height: 0.2rem;
64 | left: 0;
65 | position: absolute;
66 | transition: color, width 0.3s ease-in-out;
67 | width: 0;
68 | }
69 |
70 | &:hover {
71 | color: ${(p) =>
72 | light ? p.theme.colors.buttonLightHover : p.theme.colors.accent};
73 |
74 | &::before {
75 | width: 100%;
76 | }
77 | }
78 | `;
79 | }
80 |
81 | export const overflowEllipsis = css`
82 | overflow: hidden;
83 | white-space: nowrap;
84 | text-overflow: ellipsis;
85 | `;
86 |
--------------------------------------------------------------------------------
/docs/src/styles/theme.js:
--------------------------------------------------------------------------------
1 | const colors = {
2 | accent: '#7860ed',
3 | bg: '#ffffff',
4 | bgLight: '#f0f0f0',
5 | bgDark: '#1f1f1f',
6 | border: '#ececec',
7 | passive: '#444444',
8 | passiveBg: '#f2f2f2',
9 | textLight: '#ffffff',
10 | textDark: '#4e4e4e',
11 | button: '#4e4e4e',
12 | buttonHover: '#1f1f1f',
13 | buttonLight: '#f0f0f0',
14 | buttonLightHover: '#d6cff9',
15 | code: '#7860ed',
16 | codeBg: '#f0f7fb',
17 | };
18 |
19 | const systemFonts = [
20 | '-apple-system',
21 | 'BlinkMacSystemFont',
22 | 'Segoe UI',
23 | 'Roboto',
24 | 'Helvetica Neue',
25 | 'Arial',
26 | 'Noto Sans',
27 | 'sans-serif',
28 | 'Apple Color Emoji',
29 | 'Segoe UI Emoji',
30 | 'Segoe UI Symbol',
31 | 'Noto Color Emoji',
32 | ];
33 |
34 | const fonts = {
35 | body: systemFonts.join(', '),
36 | code: 'Space Mono, monospace',
37 | };
38 |
39 | const fontSizes = {
40 | small: '0.9em',
41 | body: '1.8rem',
42 | bodySmall: '1.5rem',
43 | code: '0.8em',
44 | h1: '3.45em',
45 | h2: '2.11em',
46 | h3: '1.64em',
47 | };
48 |
49 | const fontWeights = {
50 | body: '400',
51 | links: '500',
52 | heading: '600',
53 | };
54 |
55 | const layout = {
56 | page: '144rem',
57 | header: '4.8rem',
58 | stripes: '0.7rem',
59 | sidebar: '26rem',
60 | legend: '22rem',
61 | logo: '12rem',
62 | };
63 |
64 | const spacing = {
65 | xs: '0.6rem',
66 | sm: '1.5rem',
67 | md: '2.75rem',
68 | lg: '4.75rem',
69 | xl: '8.2rem',
70 | };
71 |
72 | export const shadows = {
73 | header: 'rgba(0, 0, 0, 0.09) 0px 2px 10px -3px',
74 | input: 'rgba(0, 0, 0, 0.09) 0px 2px 10px -3px',
75 | };
76 |
77 | const lineHeights = {
78 | body: '1.5',
79 | heading: '1.1',
80 | code: '1.2',
81 | };
82 |
83 | export const mediaSizes = {
84 | sm: 700,
85 | md: 960,
86 | lg: 1200,
87 | };
88 |
89 | export const media = {
90 | sm: `(min-width: ${mediaSizes.sm}px)`,
91 | md: `(min-width: ${mediaSizes.md}px)`,
92 | lg: `(min-width: ${mediaSizes.lg}px)`,
93 | };
94 |
95 | export const theme = {
96 | colors,
97 | fonts,
98 | fontSizes,
99 | fontWeights,
100 | layout,
101 | spacing,
102 | shadows,
103 | lineHeights,
104 | media,
105 | };
106 |
--------------------------------------------------------------------------------
/docs/src/utils/live-preview.js:
--------------------------------------------------------------------------------
1 | import {
2 | useGravity,
3 | useGravityGroup,
4 | useGravity2D,
5 | useFriction,
6 | useFrictionGroup,
7 | useFluidResistance,
8 | useFluidResistanceGroup,
9 | usePrefersReducedMotion,
10 | } from 'renature';
11 |
12 | import { Toggle } from '../components/toggle';
13 |
14 | export const scope = {
15 | useGravity,
16 | useGravityGroup,
17 | useGravity2D,
18 | useFluidResistance,
19 | useFluidResistanceGroup,
20 | useFriction,
21 | useFrictionGroup,
22 | usePrefersReducedMotion,
23 | Toggle,
24 | };
25 |
26 | const importRegex = /import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:".*?")|(?:'.*?'))[\s]*?(?:;|$|)/g;
27 |
28 | export function removeImportFromPreview(code) {
29 | return code.replace(importRegex, '');
30 | }
31 |
--------------------------------------------------------------------------------
/docs/static.config.js:
--------------------------------------------------------------------------------
1 | import Document from './src/html';
2 | import constants from './src/constants';
3 |
4 | const basePath = 'open-source/renature';
5 |
6 | export default {
7 | paths: {
8 | src: 'src',
9 | dist: `dist/${basePath}`,
10 | buildArtifacts: 'node_modules/.cache/react-static/artifacts/',
11 | devDist: 'node_modules/.cache/react-static/dist/',
12 | temp: 'node_modules/.cache/react-static/temp/',
13 | public: 'public', // The public directory (files copied to dist during build)
14 | },
15 | plugins: [
16 | [
17 | 'react-static-plugin-md-pages',
18 | {
19 | location: './content',
20 | template: './src/screens/docs',
21 | pathPrefix: 'docs',
22 | },
23 | ],
24 | 'react-static-plugin-styled-components',
25 | 'react-static-plugin-sitemap',
26 | 'react-static-plugin-react-router',
27 | ],
28 | basePath,
29 | stagingBasePath: basePath,
30 | devBasePath: basePath,
31 | Document,
32 | getSiteData: () => ({
33 | title: constants.title,
34 | }),
35 | getRoutes: async () => [
36 | {
37 | path: '/',
38 | template: require.resolve('./src/screens/home'),
39 | },
40 | {
41 | path: '/gallery',
42 | template: require.resolve('./src/screens/gallery'),
43 | },
44 | {
45 | path: '404',
46 | template: require.resolve('./src/screens/404'),
47 | },
48 | ],
49 | };
50 |
--------------------------------------------------------------------------------
/scripts/jest-transform-esm.js:
--------------------------------------------------------------------------------
1 | const { createTransformer } = require('babel-jest');
2 |
3 | module.exports = createTransformer({
4 | plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')],
5 | });
6 |
--------------------------------------------------------------------------------
/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
--------------------------------------------------------------------------------
/src/animation/configDefaults.ts:
--------------------------------------------------------------------------------
1 | import { FrictionConfig } from './friction';
2 | import { FluidResistanceConfig } from './fluidResistance';
3 | import { GravityConfig } from './gravity';
4 | import { Gravity2DParams } from './gravity2D';
5 |
6 | export const fluidResistanceDefaultConfig: FluidResistanceConfig = {
7 | mass: 10,
8 | rho: 20,
9 | area: 20,
10 | cDrag: 0.1,
11 | settle: true,
12 | };
13 |
14 | export const frictionDefaultConfig: FrictionConfig = {
15 | mu: 0.25,
16 | mass: 50,
17 | initialVelocity: 5,
18 | };
19 |
20 | export const gravityDefaultConfig: GravityConfig = {
21 | moverMass: 10000,
22 | attractorMass: 1000000000000,
23 | r: 7.5,
24 | };
25 |
26 | export const gravity2DDefaultConfig: Gravity2DParams['config'] = {
27 | attractorMass: 1000000000000,
28 | moverMass: 10000,
29 | attractorPosition: [100, 100],
30 | initialMoverPosition: [100, 0],
31 | initialMoverVelocity: [1, 0],
32 | threshold: {
33 | min: 20,
34 | max: 100,
35 | },
36 | timeScale: 100,
37 | };
38 |
--------------------------------------------------------------------------------
/src/animation/index.ts:
--------------------------------------------------------------------------------
1 | export * from './gravity';
2 | export * from './gravity2D';
3 | export * from './friction';
4 | export * from './fluidResistance';
5 | export * from './types';
6 | export * from './configDefaults';
7 |
--------------------------------------------------------------------------------
/src/animation/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties, RefObject } from 'react';
2 |
3 | import type { vector as Vector } from '../core';
4 | import type { entity as Entity } from '../forces';
5 |
6 | interface MotionVectors {
7 | position: Vector;
8 | velocity: Vector;
9 | acceleration: Vector;
10 | }
11 |
12 | export type VectorSetter = (motionVectors: MotionVectors) => void;
13 |
14 | export type Listener = (
15 | timestamp: DOMHighResTimeStamp,
16 | lastFrame: DOMHighResTimeStamp,
17 | stop: () => void
18 | ) => void;
19 |
20 | export interface Controller {
21 | start: () => void;
22 | stop: () => void;
23 | pause: () => void;
24 | set: (to: CSSProperties, i?: number) => void;
25 | }
26 |
27 | export interface AnimationParams {
28 | onUpdate: VectorSetter;
29 | onComplete: () => void;
30 | }
31 |
32 | type RepeatType = 'loop' | 'mirror';
33 |
34 | export interface HooksParams {
35 | pause?: boolean;
36 | delay?: number;
37 | repeat?: number;
38 | repeatType?: RepeatType;
39 | onFrame?: (progress: number, motionVectors: MotionVectors) => void;
40 | onAnimationComplete?: () => void;
41 | disableHardwareAcceleration?: boolean;
42 | reducedMotion?: {
43 | from: CSSProperties;
44 | to: CSSProperties;
45 | };
46 | }
47 |
48 | export type PlayState = 'forward' | 'reverse';
49 |
50 | export interface AnimatingElement<
51 | C,
52 | E extends HTMLElement | SVGElement | null = any
53 | > {
54 | ref: RefObject;
55 | config: C;
56 | onUpdate: VectorSetter;
57 | onComplete: (playState?: PlayState) => void;
58 | repeat?: number;
59 | repeatType?: RepeatType;
60 | delay?: number;
61 | pause?: boolean;
62 | }
63 |
64 | export interface StatefulAnimatingElement<
65 | C,
66 | E extends HTMLElement | SVGElement = any
67 | > extends AnimatingElement {
68 | state: {
69 | mover: Entity;
70 | attractor?: Entity;
71 | playState: PlayState;
72 | maxDistance: number;
73 | complete: boolean;
74 | paused: boolean;
75 | delayed: boolean;
76 | repeatCount: number;
77 | };
78 | }
79 |
80 | export interface AnimationCallbacks {
81 | checkReversePlayState: (
82 | animatingElement: StatefulAnimatingElement
83 | ) => void;
84 | applyForceForStep: (animatingElement: StatefulAnimatingElement) => Entity;
85 | checkStoppingCondition: (
86 | animatingElement: StatefulAnimatingElement
87 | ) => boolean;
88 | }
89 |
90 | export interface AnimationGroup {
91 | elements: StatefulAnimatingElement[];
92 | start: (c?: { isImperativeStart: boolean }) => void;
93 | pause: () => void;
94 | stop: () => void;
95 | }
96 |
97 | export type AnimationCache = Map;
98 |
--------------------------------------------------------------------------------
/src/core/Math.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as Caml_obj from "bs-platform/lib/es6/caml_obj.js";
4 |
5 | function constrainf(low, high, n) {
6 | if (Caml_obj.caml_lessthan(n, low)) {
7 | return low;
8 | } else if (Caml_obj.caml_greaterthan(n, high)) {
9 | return high;
10 | } else {
11 | return n;
12 | }
13 | }
14 |
15 | function lerpf(acc, target, roundness) {
16 | return (1.0 - roundness) * acc + roundness * target;
17 | }
18 |
19 | function remapf(param, param$1, value) {
20 | var dl = param$1[0];
21 | var rl = param[0];
22 | return dl + (param$1[1] - dl) * ((value - rl) / (param[1] - rl));
23 | }
24 |
25 | function normalizef(range, value) {
26 | return remapf(range, [
27 | 0,
28 | 1
29 | ], value);
30 | }
31 |
32 | export {
33 | constrainf ,
34 | lerpf ,
35 | remapf ,
36 | normalizef ,
37 |
38 | }
39 | /* No side effect */
40 |
--------------------------------------------------------------------------------
/src/core/Math.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Math.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const MathBS = require('./Math.bs');
10 |
11 | export const constrainf: (_1:{ readonly low: number; readonly high: number }, _2:number) => number = function (Arg1: any, Arg2: any) {
12 | const result = Curry._3(MathBS.constrainf, Arg1.low, Arg1.high, Arg2);
13 | return result
14 | };
15 |
16 | export const lerpf: (_1:{
17 | readonly acc: number;
18 | readonly target: number;
19 | readonly roundness: number
20 | }) => number = function (Arg1: any) {
21 | const result = Curry._3(MathBS.lerpf, Arg1.acc, Arg1.target, Arg1.roundness);
22 | return result
23 | };
24 |
25 | export const remapf: (_1:{
26 | readonly range: [number, number];
27 | readonly domain: [number, number];
28 | readonly value: number
29 | }) => number = function (Arg1: any) {
30 | const result = Curry._3(MathBS.remapf, Arg1.range, Arg1.domain, Arg1.value);
31 | return result
32 | };
33 |
34 | export const normalizef: (_1:{ readonly range: [number, number]; readonly value: number }) => number = function (Arg1: any) {
35 | const result = Curry._2(MathBS.normalizef, Arg1.range, Arg1.value);
36 | return result
37 | };
38 |
--------------------------------------------------------------------------------
/src/core/Math.res:
--------------------------------------------------------------------------------
1 | // Constrain a range.
2 | let constrainf = (~low, ~high, n) =>
3 | switch n {
4 | | n when n < low => low
5 | | n when n > high => high
6 | | _ => n
7 | }
8 |
9 | // Linearly interpolate a value.
10 | let lerpf = (~acc, ~target, ~roundness) => (1.0 -. roundness) *. acc +. roundness *. target
11 |
12 | // Map a value on an input range to a value on an output domain.
13 | let remapf = (~range as (rl, rh), ~domain as (dl, dh), ~value) =>
14 | dl +. (dh -. dl) *. ((value -. rl) /. (rh -. rl))
15 |
16 | // Normalize a number on an input range to an output domain of [0, 1].
17 | let normalizef = (~range, ~value) => remapf(~range, ~domain=(0., 1.), ~value)
18 |
--------------------------------------------------------------------------------
/src/core/Math.resi:
--------------------------------------------------------------------------------
1 | export constrainf: (~low: float, ~high: float, float) => float
2 |
3 | export lerpf: (~acc: float, ~target: float, ~roundness: float) => float
4 |
5 | export remapf: (~range: (float, float), ~domain: (float, float), ~value: float) => float
6 |
7 | export normalizef: (~range: (float, float), ~value: float) => float
8 |
--------------------------------------------------------------------------------
/src/core/Vector.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as $$Math from "./Math.bs.js";
4 | import * as Caml_int32 from "bs-platform/lib/es6/caml_int32.js";
5 |
6 | function add(v1, v2) {
7 | return [
8 | v1[0] + v2[0] | 0,
9 | v1[1] + v2[1] | 0
10 | ];
11 | }
12 |
13 | function addf(v1, v2) {
14 | return [
15 | v1[0] + v2[0],
16 | v1[1] + v2[1]
17 | ];
18 | }
19 |
20 | function sub(v1, v2) {
21 | return [
22 | v1[0] - v2[0] | 0,
23 | v1[1] - v2[1] | 0
24 | ];
25 | }
26 |
27 | function subf(v1, v2) {
28 | return [
29 | v1[0] - v2[0],
30 | v1[1] - v2[1]
31 | ];
32 | }
33 |
34 | function mult(v, s) {
35 | return [
36 | Math.imul(v[0], s),
37 | Math.imul(v[1], s)
38 | ];
39 | }
40 |
41 | function multf(v, s) {
42 | return [
43 | v[0] * s,
44 | v[1] * s
45 | ];
46 | }
47 |
48 | function div(v, s) {
49 | return [
50 | Caml_int32.div(v[0], s),
51 | Caml_int32.div(v[1], s)
52 | ];
53 | }
54 |
55 | function divf(v, s) {
56 | return [
57 | v[0] / s,
58 | v[1] / s
59 | ];
60 | }
61 |
62 | function mag(v) {
63 | var x = v[0];
64 | var y = v[1];
65 | return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
66 | }
67 |
68 | function magf(v) {
69 | return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2));
70 | }
71 |
72 | function norm(v) {
73 | var m = mag(v);
74 | if (m !== 0) {
75 | return divf([
76 | v[0],
77 | v[1]
78 | ], m);
79 | } else {
80 | return [
81 | 0,
82 | 0
83 | ];
84 | }
85 | }
86 |
87 | function normf(v) {
88 | var m = magf(v);
89 | if (m !== 0) {
90 | return divf(v, m);
91 | } else {
92 | return [
93 | 0,
94 | 0
95 | ];
96 | }
97 | }
98 |
99 | function lerpfV(acc, target, roundness) {
100 | var x = $$Math.lerpf(acc[0], target[0], roundness);
101 | var y = $$Math.lerpf(acc[1], target[1], roundness);
102 | return [
103 | x,
104 | y
105 | ];
106 | }
107 |
108 | function lerpV(acc, target, roundness) {
109 | var match = lerpfV([
110 | acc[0],
111 | acc[1]
112 | ], [
113 | target[0],
114 | target[1]
115 | ], roundness);
116 | return [
117 | match[0] | 0,
118 | match[1] | 0
119 | ];
120 | }
121 |
122 | export {
123 | add ,
124 | addf ,
125 | sub ,
126 | subf ,
127 | mult ,
128 | multf ,
129 | div ,
130 | divf ,
131 | mag ,
132 | magf ,
133 | norm ,
134 | normf ,
135 | lerpV ,
136 | lerpfV ,
137 |
138 | }
139 | /* No side effect */
140 |
--------------------------------------------------------------------------------
/src/core/Vector.res:
--------------------------------------------------------------------------------
1 | let foi = float_of_int;
2 | let iof = int_of_float;
3 |
4 | // The core vector type.
5 | type vector<'a> = ('a, 'a)
6 | type t<'a> = vector<'a>
7 |
8 | // Vector addition.
9 | let add = (~v1, ~v2) => (fst(v1) + fst(v2), snd(v1) + snd(v2))
10 | let addf = (~v1, ~v2) => (fst(v1) +. fst(v2), snd(v1) +. snd(v2))
11 |
12 | // Vector subtraction.
13 | let sub = (~v1, ~v2) => (fst(v1) - fst(v2), snd(v1) - snd(v2))
14 | let subf = (~v1, ~v2) => (fst(v1) -. fst(v2), snd(v1) -. snd(v2))
15 |
16 | // Vector multiplication.
17 | let mult = (~v, ~s) => (fst(v) * s, snd(v) * s)
18 | let multf = (~v, ~s) => (fst(v) *. s, snd(v) *. s)
19 |
20 | // Vector division.
21 | let div = (~v, ~s) => (fst(v) / s, snd(v) / s)
22 | let divf = (~v, ~s) => (fst(v) /. s, snd(v) /. s)
23 |
24 | // Vector magnitude.
25 | let mag = v => {
26 | let (x, y) = (fst(v) |> foi, snd(v) |> foi)
27 | sqrt(x ** 2. +. y ** 2.)
28 | }
29 |
30 | let magf = v => {
31 | let (x, y) = v
32 | sqrt(x ** 2. +. y ** 2.)
33 | }
34 |
35 | // Vector normalization.
36 | let norm = v => {
37 | let m = mag(v)
38 | switch m {
39 | | 0. => (0., 0.)
40 | | _ => divf(~v=(fst(v) |> foi, snd(v) |> foi), ~s=m)
41 | }
42 | }
43 |
44 | let normf = v => {
45 | let m = magf(v)
46 | switch m {
47 | | 0. => (0., 0.)
48 | | _ => divf(~v, ~s=m)
49 | }
50 | }
51 |
52 | // Vector linear interpolation.
53 | let lerpfV = (~acc, ~target, ~roundness) => {
54 | let (accX, accY) = acc
55 | let (targetX, targetY) = target
56 | let x = Math.lerpf(~acc=accX, ~target=targetX, ~roundness)
57 | let y = Math.lerpf(~acc=accY, ~target=targetY, ~roundness)
58 | (x, y)
59 | }
60 |
61 | let lerpV = (~acc, ~target, ~roundness) => {
62 | let (xf, yf) = lerpfV(
63 | ~acc=(fst(acc) |> foi, snd(acc) |> foi),
64 | ~target=(fst(target) |> foi, snd(target) |> foi),
65 | ~roundness,
66 | )
67 | (xf |> iof, yf |> iof)
68 | }
69 |
--------------------------------------------------------------------------------
/src/core/Vector.resi:
--------------------------------------------------------------------------------
1 | export type vector<'a> = ('a, 'a)
2 | export type t<'a> = vector<'a>
3 |
4 | export add: (~v1: vector, ~v2: vector) => vector
5 | export addf: (~v1: vector, ~v2: vector) => vector
6 |
7 | export sub: (~v1: vector, ~v2: vector) => vector
8 | export subf: (~v1: vector, ~v2: vector) => vector
9 |
10 | export mult: (~v: vector, ~s: int) => vector
11 | export multf: (~v: vector, ~s: float) => vector
12 |
13 | export div: (~v: vector, ~s: int) => vector
14 | export divf: (~v: vector, ~s: float) => vector
15 |
16 | export mag: vector => float
17 | export magf: vector => float
18 |
19 | export norm: vector => vector
20 | export normf: vector => vector
21 |
22 | export lerpV: (~acc: vector, ~target: vector, ~roundness: float) => vector
23 | export lerpfV: (~acc: vector, ~target: vector, ~roundness: float) => vector
24 |
--------------------------------------------------------------------------------
/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Vector.gen';
2 | export * from './Math.gen';
3 |
--------------------------------------------------------------------------------
/src/forces/FluidResistance.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as Vector from "../core/Vector.bs.js";
4 | import * as Gravity from "./Gravity.bs.js";
5 |
6 | function fluidResistanceForceMag(rho, velocity, area, cDrag) {
7 | var speed = Vector.magf(velocity);
8 | return 0.5 * rho * Math.pow(speed, 2) * area * cDrag;
9 | }
10 |
11 | function fluidResistanceForceV(rho, velocity, area, cDrag) {
12 | var mag = fluidResistanceForceMag(rho, velocity, area, cDrag);
13 | var dir = Vector.normf(Vector.multf(velocity, -1));
14 | return Vector.multf(dir, mag);
15 | }
16 |
17 | function getTerminalVelocity(mass, rho, cDrag, area) {
18 | return Math.sqrt(2 * mass * Gravity.gE / (rho * area * cDrag));
19 | }
20 |
21 | function getTau(mass, rho, cDrag, area) {
22 | return Math.sqrt(2 * mass / (Gravity.gE * rho * area * cDrag));
23 | }
24 |
25 | function getFluidPositionAtTime(time, mass, rho, cDrag, area) {
26 | var tv = getTerminalVelocity(mass, rho, cDrag, area);
27 | return Math.pow(tv, 2) / Gravity.gE * Math.log1p(Math.cosh(Gravity.gE * time) / tv);
28 | }
29 |
30 | function getFluidPositionAtTerminalVelocity(mass, rho, cDrag, area) {
31 | var tau = getTau(mass, rho, cDrag, area);
32 | return getFluidPositionAtTime(3 * tau, mass, rho, cDrag, area);
33 | }
34 |
35 | export {
36 | fluidResistanceForceMag ,
37 | fluidResistanceForceV ,
38 | getTau ,
39 | getFluidPositionAtTime ,
40 | getFluidPositionAtTerminalVelocity ,
41 |
42 | }
43 | /* Gravity Not a pure module */
44 |
--------------------------------------------------------------------------------
/src/forces/FluidResistance.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from FluidResistance.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const FluidResistanceBS = require('./FluidResistance.bs');
10 |
11 | import {t as Vector_t} from '../../src/core/Vector.gen';
12 |
13 | import {vector as Vector_vector} from '../../src/core/Vector.gen';
14 |
15 | export const fluidResistanceForceV: (_1:{
16 | readonly rho: number;
17 | readonly velocity: Vector_vector;
18 | readonly area: number;
19 | readonly cDrag: number
20 | }) => Vector_t = function (Arg1: any) {
21 | const result = Curry._4(FluidResistanceBS.fluidResistanceForceV, Arg1.rho, Arg1.velocity, Arg1.area, Arg1.cDrag);
22 | return result
23 | };
24 |
25 | export const getTau: (_1:{
26 | readonly mass: number;
27 | readonly rho: number;
28 | readonly cDrag: number;
29 | readonly area: number
30 | }) => number = function (Arg1: any) {
31 | const result = Curry._4(FluidResistanceBS.getTau, Arg1.mass, Arg1.rho, Arg1.cDrag, Arg1.area);
32 | return result
33 | };
34 |
35 | export const getFluidPositionAtTime: (_1:{
36 | readonly time: number;
37 | readonly mass: number;
38 | readonly rho: number;
39 | readonly cDrag: number;
40 | readonly area: number
41 | }) => number = function (Arg1: any) {
42 | const result = Curry._5(FluidResistanceBS.getFluidPositionAtTime, Arg1.time, Arg1.mass, Arg1.rho, Arg1.cDrag, Arg1.area);
43 | return result
44 | };
45 |
46 | export const getFluidPositionAtTerminalVelocity: (_1:{
47 | readonly mass: number;
48 | readonly rho: number;
49 | readonly cDrag: number;
50 | readonly area: number
51 | }) => number = function (Arg1: any) {
52 | const result = Curry._4(FluidResistanceBS.getFluidPositionAtTerminalVelocity, Arg1.mass, Arg1.rho, Arg1.cDrag, Arg1.area);
53 | return result
54 | };
55 |
--------------------------------------------------------------------------------
/src/forces/FluidResistance.res:
--------------------------------------------------------------------------------
1 | let fluidResistanceForceMag = (~rho, ~velocity, ~area, ~cDrag) => {
2 | let speed = Vector.magf(velocity)
3 |
4 | 0.5 *. rho *. speed ** 2. *. area *. cDrag
5 | }
6 |
7 | let fluidResistanceForceV = (~rho, ~velocity, ~area, ~cDrag) => {
8 | // Derive the magnitude of the drag force.
9 | let mag = fluidResistanceForceMag(~rho, ~velocity, ~area, ~cDrag)
10 |
11 | @ocaml.doc(
12 | "
13 | * Fluid resistance acts in the opposite direction of motion.
14 | * Normalize the velocity vector and multiply by -1
15 | * to derive the direction of the drag force.
16 | "
17 | )
18 | let dir = Vector.multf(~v=velocity, ~s=-1.) |> Vector.normf
19 |
20 | Vector.multf(~v=dir, ~s=mag)
21 | }
22 |
23 | @ocaml.doc(
24 | "
25 | * The terminal velocity of an object of mass, m, with frontal area A,
26 | * and a coefficient of drag C, moving through a fluid with density, ρ.
27 | "
28 | )
29 | let getTerminalVelocity = (~mass, ~rho, ~cDrag, ~area) =>
30 | sqrt(2. *. mass *. Gravity.gE /. (rho *. area *. cDrag))
31 |
32 | // The time scale, tau, along which terminal velocity is approached.
33 | let getTau = (~mass, ~rho, ~cDrag, ~area) =>
34 | sqrt(2. *. mass /. (Gravity.gE *. rho *. area *. cDrag))
35 |
36 | @ocaml.doc(
37 | "
38 | * Position at a function of time for an object of mass, m, with frontal
39 | * area A, and a coefficient of drag C, moving through a fluid with density, ρ.
40 | "
41 | )
42 | let getFluidPositionAtTime = (~time, ~mass, ~rho, ~cDrag, ~area) => {
43 | let tv = getTerminalVelocity(~mass, ~rho, ~cDrag, ~area)
44 | tv ** 2. /. Gravity.gE *. log1p(cosh(Gravity.gE *. time) /. tv)
45 | }
46 |
47 | @ocaml.doc(
48 | "
49 | * The position of an object of mass, m, with frontal area A,
50 | * and a coefficient of drag C, moving through a fluid with density, ρ.
51 | * when it has achieved 99% of terminal velocity.
52 | "
53 | )
54 | let getFluidPositionAtTerminalVelocity = (~mass, ~rho, ~cDrag, ~area) => {
55 | let tau = getTau(~mass, ~rho, ~cDrag, ~area)
56 | getFluidPositionAtTime(~time=3. *. tau, ~mass, ~rho, ~cDrag, ~area)
57 | }
58 |
--------------------------------------------------------------------------------
/src/forces/FluidResistance.resi:
--------------------------------------------------------------------------------
1 | let fluidResistanceForceMag: (
2 | ~rho: float,
3 | ~velocity: Vector.vector,
4 | ~area: float,
5 | ~cDrag: float,
6 | ) => float
7 |
8 | export fluidResistanceForceV: (
9 | ~rho: float,
10 | ~velocity: Vector.vector,
11 | ~area: float,
12 | ~cDrag: float,
13 | ) => Vector.t
14 |
15 | export getTau: (~mass: float, ~rho: float, ~cDrag: float, ~area: float) => float
16 |
17 | export getFluidPositionAtTime: (
18 | ~time: float,
19 | ~mass: float,
20 | ~rho: float,
21 | ~cDrag: float,
22 | ~area: float,
23 | ) => float
24 |
25 | export getFluidPositionAtTerminalVelocity: (
26 | ~mass: float,
27 | ~rho: float,
28 | ~cDrag: float,
29 | ~area: float,
30 | ) => float
31 |
--------------------------------------------------------------------------------
/src/forces/Force.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as Vector from "../core/Vector.bs.js";
4 |
5 | function applyForce(force, entity, time) {
6 | var nextAcceleration = Vector.divf(force, entity.mass);
7 | var nextVelocity = Vector.addf(entity.velocity, Vector.multf(nextAcceleration, time));
8 | var nextPosition = Vector.addf(entity.position, Vector.multf(nextVelocity, time));
9 | return {
10 | mass: entity.mass,
11 | acceleration: nextAcceleration,
12 | velocity: nextVelocity,
13 | position: nextPosition
14 | };
15 | }
16 |
17 | export {
18 | applyForce ,
19 |
20 | }
21 | /* No side effect */
22 |
--------------------------------------------------------------------------------
/src/forces/Force.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Force.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const ForceBS = require('./Force.bs');
10 |
11 | import {t as Vector_t} from '../../src/core/Vector.gen';
12 |
13 | // tslint:disable-next-line:interface-over-type-literal
14 | export type entity = {
15 | readonly mass: number;
16 | readonly acceleration: Vector_t;
17 | readonly velocity: Vector_t;
18 | readonly position: Vector_t
19 | };
20 |
21 | export const applyForce: (_1:{
22 | readonly force: Vector_t;
23 | readonly entity: entity;
24 | readonly time: number
25 | }) => entity = function (Arg1: any) {
26 | const result = Curry._3(ForceBS.applyForce, Arg1.force, Arg1.entity, Arg1.time);
27 | return result
28 | };
29 |
--------------------------------------------------------------------------------
/src/forces/Force.res:
--------------------------------------------------------------------------------
1 | type entity = {
2 | mass: float,
3 | acceleration: Vector.t,
4 | velocity: Vector.t,
5 | position: Vector.t,
6 | }
7 |
8 | let applyForce = (~force, ~entity, ~time) => {
9 | // Derive the acceleration created by the force and add it to the current acceleration.
10 | let nextAcceleration = Vector.divf(~v=force, ~s=entity.mass)
11 |
12 | // Add the acceleration to the current velocity.
13 | let nextVelocity = Vector.addf(
14 | ~v1=entity.velocity,
15 | ~v2=Vector.multf(~v=nextAcceleration, ~s=time),
16 | )
17 |
18 | // Add the velocity to the position.
19 | let nextPosition = Vector.addf(~v1=entity.position, ~v2=Vector.multf(~v=nextVelocity, ~s=time))
20 |
21 | {
22 | mass: entity.mass,
23 | acceleration: nextAcceleration,
24 | velocity: nextVelocity,
25 | position: nextPosition,
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/forces/Force.resi:
--------------------------------------------------------------------------------
1 | export type entity = {
2 | mass: float,
3 | acceleration: Vector.t,
4 | velocity: Vector.t,
5 | position: Vector.t,
6 | }
7 |
8 | export applyForce: (~force: Vector.t, ~entity: entity, ~time: float) => entity
9 |
--------------------------------------------------------------------------------
/src/forces/Friction.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as Vector from "../core/Vector.bs.js";
4 | import * as Gravity from "./Gravity.bs.js";
5 |
6 | function frictionForceMag(mu, mass) {
7 | return mu * Gravity.gE * mass;
8 | }
9 |
10 | function frictionForceV(mu, mass, velocity) {
11 | var mag = frictionForceMag(mu, mass);
12 | var dir = Vector.normf(Vector.multf(velocity, -1));
13 | return Vector.multf(dir, mag);
14 | }
15 |
16 | function getMaxDistanceFriction(mu, initialVelocity) {
17 | var accelerationF = -1 * mu * Gravity.gE;
18 | return Math.pow(initialVelocity, 2) / (-2 * accelerationF);
19 | }
20 |
21 | export {
22 | frictionForceMag ,
23 | frictionForceV ,
24 | getMaxDistanceFriction ,
25 |
26 | }
27 | /* Gravity Not a pure module */
28 |
--------------------------------------------------------------------------------
/src/forces/Friction.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Friction.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const FrictionBS = require('./Friction.bs');
10 |
11 | import {t as Vector_t} from '../../src/core/Vector.gen';
12 |
13 | export const frictionForceMag: (_1:{ readonly mu: number; readonly mass: number }) => number = function (Arg1: any) {
14 | const result = Curry._2(FrictionBS.frictionForceMag, Arg1.mu, Arg1.mass);
15 | return result
16 | };
17 |
18 | export const frictionForceV: (_1:{
19 | readonly mu: number;
20 | readonly mass: number;
21 | readonly velocity: Vector_t
22 | }) => Vector_t = function (Arg1: any) {
23 | const result = Curry._3(FrictionBS.frictionForceV, Arg1.mu, Arg1.mass, Arg1.velocity);
24 | return result
25 | };
26 |
27 | export const getMaxDistanceFriction: (_1:{ readonly mu: number; readonly initialVelocity: number }) => number = function (Arg1: any) {
28 | const result = Curry._2(FrictionBS.getMaxDistanceFriction, Arg1.mu, Arg1.initialVelocity);
29 | return result
30 | };
31 |
--------------------------------------------------------------------------------
/src/forces/Friction.res:
--------------------------------------------------------------------------------
1 | // The magnitude of the friction force.
2 | let frictionForceMag = (~mu, ~mass) => mu *. Gravity.gE *. mass
3 |
4 | // The frictional force vector.
5 | let frictionForceV = (~mu, ~mass, ~velocity) => {
6 | // Derive the magnitude of the frictive force.
7 | let mag = frictionForceMag(~mu, ~mass)
8 |
9 | @ocaml.doc(
10 | "
11 | * Friction acts in the opposite direction of motion.
12 | * Normalize the velocity vector and multiply by -1
13 | * to derive the direction of the frictive force.
14 | "
15 | )
16 | let dir = Vector.multf(~v=velocity, ~s=-1.) |> Vector.normf
17 |
18 | Vector.multf(~v=dir, ~s=mag)
19 | }
20 |
21 | @ocaml.doc(
22 | "
23 | * The kinematic equation for deriving distance traveled
24 | * by a body to reach rest assuming constant acceleration.
25 | "
26 | )
27 | let getMaxDistanceFriction = (~mu, ~initialVelocity) => {
28 | let accelerationF = -1. *. mu *. Gravity.gE
29 |
30 | initialVelocity ** 2. /. (-2. *. accelerationF)
31 | }
32 |
--------------------------------------------------------------------------------
/src/forces/Friction.resi:
--------------------------------------------------------------------------------
1 | export frictionForceMag: (~mu: float, ~mass: float) => float
2 |
3 | export frictionForceV: (~mu: float, ~mass: float, ~velocity: Vector.t) => Vector.t
4 |
5 | export getMaxDistanceFriction: (~mu: float, ~initialVelocity: float) => float
6 |
--------------------------------------------------------------------------------
/src/forces/Gravity.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as $$Math from "../core/Math.bs.js";
4 | import * as Vector from "../core/Vector.bs.js";
5 |
6 | var gU = 6.67428 * Math.pow(10, -11);
7 |
8 | function gravityForceMag(attractorMass, moverMass, r, gOpt, param) {
9 | var g = gOpt !== undefined ? gOpt : gU;
10 | return g * attractorMass * moverMass / Math.pow(r, 2);
11 | }
12 |
13 | function gravityForceV(attractorMass, moverMass, attractor, mover, gOpt, threshold, param) {
14 | var g = gOpt !== undefined ? gOpt : gU;
15 | var v = Vector.subf(attractor, mover);
16 | var mag = Vector.magf(v);
17 | var distance = threshold !== undefined ? $$Math.constrainf(threshold[0], threshold[1], mag) : mag;
18 | var dir = Vector.normf(v);
19 | return Vector.multf(dir, gravityForceMag(attractorMass, moverMass, distance, g, undefined));
20 | }
21 |
22 | var gE = 9.80665;
23 |
24 | export {
25 | gU ,
26 | gE ,
27 | gravityForceMag ,
28 | gravityForceV ,
29 |
30 | }
31 | /* gU Not a pure module */
32 |
--------------------------------------------------------------------------------
/src/forces/Gravity.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Gravity.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const GravityBS = require('./Gravity.bs');
10 |
11 | import {t as Vector_t} from '../../src/core/Vector.gen';
12 |
13 | export const gU: number = GravityBS.gU;
14 |
15 | export const gE: number = GravityBS.gE;
16 |
17 | export const gravityForceMag: (_1:{
18 | readonly attractorMass: number;
19 | readonly moverMass: number;
20 | readonly r: number;
21 | readonly g?: number
22 | }, _2:void) => number = function (Arg1: any, Arg2: any) {
23 | const result = Curry._5(GravityBS.gravityForceMag, Arg1.attractorMass, Arg1.moverMass, Arg1.r, Arg1.g, Arg2);
24 | return result
25 | };
26 |
27 | export const gravityForceV: (_1:{
28 | readonly attractorMass: number;
29 | readonly moverMass: number;
30 | readonly attractor: Vector_t;
31 | readonly mover: Vector_t;
32 | readonly g?: number;
33 | readonly threshold?: [number, number]
34 | }, _2:void) => Vector_t = function (Arg1: any, Arg2: any) {
35 | const result = Curry._7(GravityBS.gravityForceV, Arg1.attractorMass, Arg1.moverMass, Arg1.attractor, Arg1.mover, Arg1.g, Arg1.threshold, Arg2);
36 | return result
37 | };
38 |
--------------------------------------------------------------------------------
/src/forces/Gravity.res:
--------------------------------------------------------------------------------
1 | // The Universal Gravitational Constant, G.
2 | let gU = 6.67428 *. 10. ** -11.
3 |
4 | // The acceleration due to gravity at Earth's surface.
5 | let gE = 9.80665
6 |
7 | // The magnitude of the gravitational force.
8 | let gravityForceMag = (~attractorMass, ~moverMass, ~r, ~g=gU, ()) =>
9 | g *. attractorMass *. moverMass /. r ** 2.
10 |
11 | // The gravitational force vector.
12 | let gravityForceV = (~attractorMass, ~moverMass, ~attractor, ~mover, ~g=gU, ~threshold=?, ()) => {
13 | // Derive the vector pointing from attractor to mover.
14 | let v = Vector.subf(~v1=attractor, ~v2=mover)
15 |
16 | // Derive the magnitude of the above vector.
17 | let mag = Vector.magf(v)
18 |
19 | // Constrain the gravitational force if threshold constraints were supplied.
20 | let distance = switch threshold {
21 | | Some(th) => Math.constrainf(~low=fst(th), ~high=snd(th), mag)
22 | | None => mag
23 | }
24 |
25 | // Derive the unit vector of the above vector.
26 | let dir = Vector.normf(v)
27 |
28 | // Multiply the unit vector by the size of the force.
29 | Vector.multf(~v=dir, ~s=gravityForceMag(~attractorMass, ~moverMass, ~r=distance, ~g, ()))
30 | }
31 |
--------------------------------------------------------------------------------
/src/forces/Gravity.resi:
--------------------------------------------------------------------------------
1 | export gU: float
2 |
3 | export gE: float
4 |
5 | export gravityForceMag: (
6 | ~attractorMass: float,
7 | ~moverMass: float,
8 | ~r: float,
9 | ~g: float=?,
10 | unit,
11 | ) => float
12 |
13 | export gravityForceV: (
14 | ~attractorMass: float,
15 | ~moverMass: float,
16 | ~attractor: Vector.t,
17 | ~mover: Vector.t,
18 | ~g: float=?,
19 | ~threshold: (float, float)=?,
20 | unit,
21 | ) => Vector.t
22 |
--------------------------------------------------------------------------------
/src/forces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Force.gen';
2 | export * from './Gravity.gen';
3 | export * from './Friction.gen';
4 | export * from './FluidResistance.gen';
5 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useGravity';
2 | export * from './useGravityGroup';
3 | export * from './useGravity2D';
4 | export * from './useFriction';
5 | export * from './useFrictionGroup';
6 | export * from './useFluidResistance';
7 | export * from './useFluidResistanceGroup';
8 | export * from './usePrefersReducedMotion';
9 |
--------------------------------------------------------------------------------
/src/hooks/useFluidResistance.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import { Controller, fluidResistanceDefaultConfig } from '../animation';
4 |
5 | import {
6 | useFluidResistanceGroup,
7 | UseFluidResistanceParams,
8 | } from './useFluidResistanceGroup';
9 |
10 | export const useFluidResistance = ({
11 | from,
12 | to,
13 | config = fluidResistanceDefaultConfig,
14 | pause = false,
15 | delay,
16 | repeat,
17 | repeatType = 'mirror',
18 | onFrame,
19 | onAnimationComplete,
20 | disableHardwareAcceleration,
21 | reducedMotion,
22 | }: UseFluidResistanceParams): [{ ref: RefObject }, Controller] => {
23 | const [props, controller] = useFluidResistanceGroup(1, () => ({
24 | from,
25 | to,
26 | config,
27 | pause,
28 | delay,
29 | repeat,
30 | repeatType,
31 | onFrame,
32 | onAnimationComplete,
33 | disableHardwareAcceleration,
34 | reducedMotion,
35 | }));
36 |
37 | return [props[0], controller];
38 | };
39 |
--------------------------------------------------------------------------------
/src/hooks/useFluidResistanceGroup.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import type { CSSPairs } from '../parsers';
4 | import {
5 | FluidResistanceConfig,
6 | HooksParams,
7 | Controller,
8 | fluidResistanceDefaultConfig,
9 | fluidResistanceGroup,
10 | } from '../animation';
11 | import { getFluidPositionAtTerminalVelocity } from '../forces';
12 |
13 | import { useForceGroup } from './useForceGroup';
14 |
15 | export type UseFluidResistanceParams = CSSPairs &
16 | HooksParams & {
17 | config?: FluidResistanceConfig;
18 | };
19 |
20 | export const useFluidResistanceGroup = <
21 | E extends HTMLElement | SVGElement = any
22 | >(
23 | n: number,
24 | fn: (index: number) => UseFluidResistanceParams
25 | ): [{ ref: RefObject }[], Controller] =>
26 | useForceGroup({
27 | n,
28 | fn,
29 | defaultConfig: fluidResistanceDefaultConfig,
30 | getMaxDistance: getFluidPositionAtTerminalVelocity,
31 | deriveGroup: fluidResistanceGroup,
32 | dimension: 'y',
33 | });
34 |
--------------------------------------------------------------------------------
/src/hooks/useFriction.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import { Controller, frictionDefaultConfig } from '../animation';
4 |
5 | import { useFrictionGroup, UseFrictionParams } from './useFrictionGroup';
6 |
7 | export const useFriction = ({
8 | from,
9 | to,
10 | config = frictionDefaultConfig,
11 | pause = false,
12 | delay,
13 | repeat,
14 | repeatType = 'mirror',
15 | onFrame,
16 | onAnimationComplete,
17 | disableHardwareAcceleration = false,
18 | reducedMotion,
19 | }: UseFrictionParams): [{ ref: RefObject }, Controller] => {
20 | const [props, controller] = useFrictionGroup(1, () => ({
21 | from,
22 | to,
23 | config,
24 | pause,
25 | delay,
26 | repeat,
27 | repeatType,
28 | onFrame,
29 | onAnimationComplete,
30 | disableHardwareAcceleration,
31 | reducedMotion,
32 | }));
33 |
34 | return [props[0], controller];
35 | };
36 |
--------------------------------------------------------------------------------
/src/hooks/useFrictionGroup.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import type { CSSPairs } from '../parsers';
4 | import {
5 | FrictionConfig,
6 | frictionDefaultConfig,
7 | frictionGroup,
8 | HooksParams,
9 | Controller,
10 | } from '../animation';
11 | import { getMaxDistanceFriction } from '../forces';
12 |
13 | import { useForceGroup } from './useForceGroup';
14 |
15 | export type UseFrictionParams = CSSPairs &
16 | HooksParams & {
17 | config?: FrictionConfig;
18 | };
19 |
20 | export const useFrictionGroup = (
21 | n: number,
22 | fn: (index: number) => UseFrictionParams
23 | ): [{ ref: RefObject }[], Controller] =>
24 | useForceGroup({
25 | n,
26 | fn,
27 | defaultConfig: frictionDefaultConfig,
28 | getMaxDistance: getMaxDistanceFriction,
29 | deriveGroup: frictionGroup,
30 | dimension: 'x',
31 | });
32 |
--------------------------------------------------------------------------------
/src/hooks/useGravity.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import { Controller, gravityDefaultConfig } from '../animation';
4 |
5 | import { useGravityGroup, UseGravityParams } from './useGravityGroup';
6 |
7 | export const useGravity = ({
8 | from,
9 | to,
10 | config = gravityDefaultConfig,
11 | pause = false,
12 | delay,
13 | repeat,
14 | repeatType = 'mirror',
15 | onFrame,
16 | onAnimationComplete,
17 | disableHardwareAcceleration,
18 | reducedMotion,
19 | }: UseGravityParams): [{ ref: RefObject }, Controller] => {
20 | const [props, controller] = useGravityGroup(1, () => ({
21 | from,
22 | to,
23 | config,
24 | pause,
25 | delay,
26 | repeat,
27 | repeatType,
28 | onFrame,
29 | onAnimationComplete,
30 | disableHardwareAcceleration,
31 | reducedMotion,
32 | }));
33 |
34 | return [props[0], controller];
35 | };
36 |
--------------------------------------------------------------------------------
/src/hooks/useGravity2D.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useRef, useMemo, useLayoutEffect } from 'react';
2 |
3 | import {
4 | Gravity2DParams,
5 | gravity2D,
6 | Controller,
7 | Gravity2DController,
8 | gravity2DDefaultConfig,
9 | VectorSetter,
10 | } from '../animation';
11 |
12 | type UseGravity2DArgs = {
13 | config?: Gravity2DParams['config'];
14 | pause?: boolean;
15 | delay?: number;
16 | onFrame?: VectorSetter;
17 | onAnimationComplete?: () => void;
18 | disableHardwareAcceleration?: boolean;
19 | };
20 |
21 | export const useGravity2D = ({
22 | config = gravity2DDefaultConfig,
23 | pause = false,
24 | delay,
25 | onFrame,
26 | onAnimationComplete,
27 | disableHardwareAcceleration = false,
28 | }: UseGravity2DArgs): [
29 | { ref: RefObject },
30 | Controller & Gravity2DController
31 | ] => {
32 | /**
33 | * Store a ref to the mover element we'll be animating.
34 | * A user will spread this ref onto their own element, which
35 | * is what allows us to directly update the style property
36 | * without triggering rerenders.
37 | */
38 | const moverRef = useRef(null);
39 |
40 | const { controller } = useMemo(
41 | () =>
42 | gravity2D({
43 | config,
44 | onUpdate: ({ position, velocity, acceleration }) => {
45 | moverRef.current &&
46 | (moverRef.current.style.transform = `translate(${position[0]}px, ${
47 | position[1]
48 | }px) translate(-50%, -50%)${
49 | disableHardwareAcceleration ? '' : ' translateZ(0)'
50 | }`);
51 |
52 | if (onFrame) {
53 | onFrame({ position, velocity, acceleration });
54 | }
55 | },
56 | onComplete: () => {
57 | if (onAnimationComplete) {
58 | onAnimationComplete();
59 | }
60 | },
61 | }),
62 | [config, onFrame, onAnimationComplete, disableHardwareAcceleration]
63 | );
64 |
65 | useLayoutEffect(() => {
66 | if (!pause && !delay) {
67 | controller.start();
68 | }
69 |
70 | let timerId: number;
71 | if (!pause && delay) {
72 | timerId = window.setTimeout(() => {
73 | controller.start();
74 | }, delay);
75 | }
76 |
77 | return () => {
78 | timerId && window.clearTimeout(timerId);
79 |
80 | // Ensure we cancel any running animation on unmount.
81 | controller.stop();
82 | };
83 | }, [pause, delay, controller]);
84 |
85 | return [{ ref: moverRef }, controller];
86 | };
87 |
--------------------------------------------------------------------------------
/src/hooks/useGravityGroup.ts:
--------------------------------------------------------------------------------
1 | import type { RefObject } from 'react';
2 |
3 | import type { CSSPairs } from '../parsers';
4 | import {
5 | GravityConfig,
6 | gravityDefaultConfig,
7 | gravityGroup,
8 | HooksParams,
9 | Controller,
10 | } from '../animation';
11 |
12 | import { useForceGroup } from './useForceGroup';
13 |
14 | export type UseGravityParams = CSSPairs &
15 | HooksParams & {
16 | config?: GravityConfig;
17 | };
18 |
19 | export const useGravityGroup = (
20 | n: number,
21 | fn: (index: number) => UseGravityParams
22 | ): [{ ref: RefObject }[], Controller] =>
23 | useForceGroup({
24 | n,
25 | fn,
26 | defaultConfig: gravityDefaultConfig,
27 | getMaxDistance: (config) => config.r,
28 | deriveGroup: gravityGroup,
29 | dimension: 'x',
30 | });
31 |
--------------------------------------------------------------------------------
/src/hooks/usePrefersReducedMotion.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const query = '(prefers-reduced-motion: no-preference)';
4 |
5 | const getInitialState = () =>
6 | typeof window === 'undefined' ? true : !window.matchMedia(query).matches;
7 |
8 | export const usePrefersReducedMotion = (): boolean => {
9 | const [prefersReducedMotion, setPrefersReducedMotion] = useState(
10 | getInitialState()
11 | );
12 |
13 | useEffect(() => {
14 | const mediaQueryList = window.matchMedia(query);
15 |
16 | const listener = (event: MediaQueryListEvent) => {
17 | setPrefersReducedMotion(!event.matches);
18 | };
19 |
20 | mediaQueryList.addEventListener('change', listener);
21 |
22 | return () => {
23 | mediaQueryList.removeEventListener('change', listener);
24 | };
25 | }, []);
26 |
27 | return prefersReducedMotion;
28 | };
29 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hooks';
2 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_box_shadow.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Interpolate_box_shadow.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const Interpolate_box_shadowBS = require('./Interpolate_box_shadow.bs');
10 |
11 | export const interpolateBoxShadow: (_1:{
12 | readonly range: [number, number];
13 | readonly domain: [string, string];
14 | readonly value: number
15 | }) => string = function (Arg1: any) {
16 | const result = Curry._3(Interpolate_box_shadowBS.interpolateBoxShadow, Arg1.range, Arg1.domain, Arg1.value);
17 | return result
18 | };
19 |
20 | export const interpolateBoxShadows: (_1:{
21 | readonly range: [number, number];
22 | readonly domain: [string, string];
23 | readonly value: number
24 | }) => string = function (Arg1: any) {
25 | const result = Curry._3(Interpolate_box_shadowBS.interpolateBoxShadows, Arg1.range, Arg1.domain, Arg1.value);
26 | return result
27 | };
28 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_box_shadow.res:
--------------------------------------------------------------------------------
1 | @bs.module("../parsers/normalize-color")
2 | external normalizeColor: string => Js.Nullable.t = "normalizeColor"
3 |
4 | @bs.module("../parsers/normalize-color")
5 | external rgba: float => Parse_color.rgba = "rgba"
6 |
7 | let colorToRgba = color =>
8 | color
9 | ->normalizeColor
10 | ->Js.Nullable.toOption
11 | ->Belt.Option.map(rgba)
12 | ->Belt.Option.getWithDefault({
13 | open Parse_color
14 | {r: 0., g: 0., b: 0., a: 1.}
15 | })
16 |
17 | let interpolateBoxShadow = (~range as (rl, rh), ~domain as (dl, dh), ~value) => {
18 | // Parse the from and to box-shadows.
19 | let fromBoxShadow = Parse_box_shadow.parseBoxShadow(dl)
20 | let toBoxShadow = Parse_box_shadow.parseBoxShadow(dh)
21 |
22 | // Interpolate each property in the box-shadow individually.
23 | let offsetX = Interpolate_unit.interpolateUnit(
24 | ~range=(rl, rh),
25 | ~domain=(fromBoxShadow.offsetX, toBoxShadow.offsetX),
26 | ~value,
27 | )
28 |
29 | let offsetY = Interpolate_unit.interpolateUnit(
30 | ~range=(rl, rh),
31 | ~domain=(fromBoxShadow.offsetY, toBoxShadow.offsetY),
32 | ~value,
33 | )
34 |
35 | let blur = Interpolate_unit.interpolateUnit(
36 | ~range=(rl, rh),
37 | ~domain=(fromBoxShadow.blur, toBoxShadow.blur),
38 | ~value,
39 | )
40 |
41 | let spread = Interpolate_unit.interpolateUnit(
42 | ~range=(rl, rh),
43 | ~domain=(fromBoxShadow.spread, toBoxShadow.spread),
44 | ~value,
45 | )
46 |
47 | let color = Interpolate_color.interpolateColor(
48 | ~range=(rl, rh),
49 | ~domain=(colorToRgba(fromBoxShadow.color), colorToRgba(toBoxShadow.color)),
50 | ~value,
51 | )
52 |
53 | let inset = fromBoxShadow.inset && toBoxShadow.inset ? "inset " : ""
54 |
55 | inset ++ ([offsetX, offsetY, blur, spread, color] |> Js.Array.joinWith(" "))
56 | }
57 |
58 | let interpolateBoxShadows = (~range, ~domain as (dl, dh), ~value) => {
59 | let dlBoxShadows = Parse_box_shadow.parseBoxShadows(dl)
60 | let dhBoxShadows = Parse_box_shadow.parseBoxShadows(dh)
61 |
62 | dlBoxShadows
63 | |> Array.mapi((i, bsl) => interpolateBoxShadow(~range, ~domain=(bsl, dhBoxShadows[i]), ~value))
64 | |> Js.Array.joinWith(", ")
65 | }
66 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_box_shadow.resi:
--------------------------------------------------------------------------------
1 | export interpolateBoxShadow: (
2 | ~range: (float, float),
3 | ~domain: (string, string),
4 | ~value: float,
5 | ) => string
6 |
7 | export interpolateBoxShadows: (
8 | ~range: (float, float),
9 | ~domain: (string, string),
10 | ~value: float,
11 | ) => string
12 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_color.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as $$Math from "../core/Math.bs.js";
4 |
5 | function lerpColor(acc, target, roundness) {
6 | return {
7 | r: $$Math.lerpf(acc.r, target.r, roundness),
8 | g: $$Math.lerpf(acc.g, target.g, roundness),
9 | b: $$Math.lerpf(acc.b, target.b, roundness),
10 | a: $$Math.lerpf(acc.a, target.a, roundness)
11 | };
12 | }
13 |
14 | function interpolateColor(param, param$1, value) {
15 | var rl = param[0];
16 | var progress = (value - rl) / (param[1] - rl);
17 | var match = lerpColor(param$1[0], param$1[1], progress);
18 | var rInt = match.r | 0;
19 | var gInt = match.g | 0;
20 | var bInt = match.b | 0;
21 | return "rgba(" + rInt + ", " + gInt + ", " + bInt + ", " + match.a + ")";
22 | }
23 |
24 | export {
25 | lerpColor ,
26 | interpolateColor ,
27 |
28 | }
29 | /* No side effect */
30 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_color.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Interpolate_color.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const Interpolate_colorBS = require('./Interpolate_color.bs');
10 |
11 | import {rgba as Parse_color_rgba} from '../../src/parsers/Parse_color.gen';
12 |
13 | export const lerpColor: (_1:{
14 | readonly acc: Parse_color_rgba;
15 | readonly target: Parse_color_rgba;
16 | readonly roundness: number
17 | }) => Parse_color_rgba = function (Arg1: any) {
18 | const result = Curry._3(Interpolate_colorBS.lerpColor, Arg1.acc, Arg1.target, Arg1.roundness);
19 | return result
20 | };
21 |
22 | export const interpolateColor: (_1:{
23 | readonly range: [number, number];
24 | readonly domain: [Parse_color_rgba, Parse_color_rgba];
25 | readonly value: number
26 | }) => string = function (Arg1: any) {
27 | const result = Curry._3(Interpolate_colorBS.interpolateColor, Arg1.range, Arg1.domain, Arg1.value);
28 | return result
29 | };
30 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_color.res:
--------------------------------------------------------------------------------
1 | let iof = int_of_float;
2 |
3 | let lerpColor = (~acc, ~target, ~roundness) => {
4 | open Parse_color
5 | {
6 | r: Math.lerpf(~acc=acc.r, ~target=target.r, ~roundness),
7 | g: Math.lerpf(~acc=acc.g, ~target=target.g, ~roundness),
8 | b: Math.lerpf(~acc=acc.b, ~target=target.b, ~roundness),
9 | a: Math.lerpf(~acc=acc.a, ~target=target.a, ~roundness),
10 | }
11 | }
12 |
13 | let interpolateColor = (~range as (rl, rh), ~domain as (dl, dh), ~value) => {
14 | let progress = (value -. rl) /. (rh -. rl)
15 | let {Parse_color.r: r, g, b, a} = lerpColor(~acc=dl, ~target=dh, ~roundness=progress)
16 | let (rInt, gInt, bInt) = (iof(r), iof(g), iof(b))
17 | j`rgba($rInt, $gInt, $bInt, $a)`
18 | }
19 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_color.resi:
--------------------------------------------------------------------------------
1 | export lerpColor: (
2 | ~acc: Parse_color.rgba,
3 | ~target: Parse_color.rgba,
4 | ~roundness: float,
5 | ) => Parse_color.rgba
6 |
7 | export interpolateColor: (
8 | ~range: (float, float),
9 | ~domain: (Parse_color.rgba, Parse_color.rgba),
10 | ~value: float,
11 | ) => string
12 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_transform.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Interpolate_transform.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const Interpolate_transformBS = require('./Interpolate_transform.bs');
10 |
11 | export const interpolateTransform: (_1:{
12 | readonly range: [number, number];
13 | readonly domain: [string, string];
14 | readonly value: number
15 | }) => string = function (Arg1: any) {
16 | const result = Curry._3(Interpolate_transformBS.interpolateTransform, Arg1.range, Arg1.domain, Arg1.value);
17 | return result
18 | };
19 |
20 | export const interpolateTransforms: (_1:{
21 | readonly range: [number, number];
22 | readonly domain: [string, string];
23 | readonly value: number
24 | }) => string = function (Arg1: any) {
25 | const result = Curry._3(Interpolate_transformBS.interpolateTransforms, Arg1.range, Arg1.domain, Arg1.value);
26 | return result
27 | };
28 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_transform.res:
--------------------------------------------------------------------------------
1 | let interpolateTransform = (~range as (rl, rh), ~domain as (dl, dh), ~value) => {
2 | let fromTransform = Parse_transform.parseTransform(dl)
3 | let toTransform = Parse_transform.parseTransform(dh)
4 |
5 | let transforms = switch (
6 | fromTransform.transform->Js.Nullable.toOption,
7 | toTransform.transform->Js.Nullable.toOption,
8 | ) {
9 | | (Some(tl), Some(th)) =>
10 | let tl_1 = Js.String.split(",", tl) |> Array.map(Js.String.trim)
11 | let th_1 = Js.String.split(",", th) |> Array.map(Js.String.trim)
12 |
13 | tl_1 |> Array.mapi((i, t) =>
14 | Interpolate_unit.interpolateUnit(~range=(rl, rh), ~domain=(t, th_1[i]), ~value)
15 | )
16 | | (None, Some(_)) =>
17 | Js.Exn.raiseError("The transform for from: '" ++ dl ++ "' could not be parsed.")
18 | | (Some(_), None) =>
19 | Js.Exn.raiseError("The transform for to: '" ++ dh ++ "' could not be parsed.")
20 | | (None, None) =>
21 | Js.Exn.raiseError(
22 | "The transforms for from: '" ++ dl ++ "' and to: '" ++ dh ++ "' could not be pased.",
23 | )
24 | }
25 |
26 | fromTransform.transformProperty->Js.Nullable.toOption->Belt.Option.getWithDefault("") ++
27 | ("(" ++
28 | ((transforms |> Js.Array.joinWith(", ")) ++ ")"))
29 | }
30 |
31 | let populateTransformRegistry = transforms => {
32 | transforms |> Js.Array.reduce((registry, t) => {
33 | let property = Js.String.substring(~from=0, ~to_=Js.String.indexOf("(", t), t)
34 |
35 | Js.Dict.set(registry, property, t)
36 |
37 | registry
38 | }, Js.Dict.empty())
39 | }
40 |
41 | let interpolateTransforms = (~range, ~domain as (dl, dh), ~value) => {
42 | let dlTransforms = Parse_transform.parseTransforms(dl)
43 | let dhTransforms = Parse_transform.parseTransforms(dh)
44 |
45 | let dlTransformRegistry = populateTransformRegistry(dlTransforms)
46 | let dhTransfromRegistry = populateTransformRegistry(dhTransforms)
47 |
48 | dlTransformRegistry
49 | |> Js.Dict.entries
50 | |> Js.Array.map(((property, t)) =>
51 | interpolateTransform(
52 | ~range,
53 | ~domain=(t, Js.Dict.unsafeGet(dhTransfromRegistry, property)),
54 | ~value,
55 | )
56 | )
57 | |> Js.Array.joinWith(" ")
58 | }
59 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_transform.resi:
--------------------------------------------------------------------------------
1 | export interpolateTransform: (
2 | ~range: (float, float),
3 | ~domain: (string, string),
4 | ~value: float,
5 | ) => string
6 |
7 | export interpolateTransforms: (
8 | ~range: (float, float),
9 | ~domain: (string, string),
10 | ~value: float,
11 | ) => string
12 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_unit.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as $$Math from "../core/Math.bs.js";
4 | import * as Parse_unit from "../parsers/Parse_unit.bs.js";
5 |
6 | function interpolateUnit(param, param$1, value) {
7 | var rl = param[0];
8 | var match = Parse_unit.parseUnit(param$1[0]);
9 | var dlUnit = match.unit;
10 | var match$1 = Parse_unit.parseUnit(param$1[1]);
11 | var progress = (value - rl) / (param[1] - rl);
12 | var output = $$Math.lerpf(match.value, match$1.value, progress);
13 | if (dlUnit == null) {
14 | return output.toString();
15 | } else {
16 | return output.toString() + dlUnit;
17 | }
18 | }
19 |
20 | export {
21 | interpolateUnit ,
22 |
23 | }
24 | /* No side effect */
25 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_unit.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Interpolate_unit.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const Interpolate_unitBS = require('./Interpolate_unit.bs');
10 |
11 | export const interpolateUnit: (_1:{
12 | readonly range: [number, number];
13 | readonly domain: [string, string];
14 | readonly value: number
15 | }) => string = function (Arg1: any) {
16 | const result = Curry._3(Interpolate_unitBS.interpolateUnit, Arg1.range, Arg1.domain, Arg1.value);
17 | return result
18 | };
19 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_unit.res:
--------------------------------------------------------------------------------
1 | let sof = Js.Float.toString;
2 |
3 | let interpolateUnit = (~range as (rl, rh), ~domain as (dl, dh), ~value) => {
4 | let {Parse_unit.value: dlNum, unit: dlUnit} = Parse_unit.parseUnit(dl)
5 | let {Parse_unit.value: dhNum} = Parse_unit.parseUnit(dh)
6 |
7 | let progress = (value -. rl) /. (rh -. rl)
8 | let output = Math.lerpf(~acc=dlNum, ~target=dhNum, ~roundness=progress)
9 |
10 | switch dlUnit->Js.Nullable.toOption {
11 | | Some(u) => sof(output) ++ u
12 | | None => sof(output)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/interpolaters/Interpolate_unit.resi:
--------------------------------------------------------------------------------
1 | export interpolateUnit: (~range: (float, float), ~domain: (string, string), ~value: float) => string
2 |
--------------------------------------------------------------------------------
/src/interpolaters/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Interpolate_color.gen';
2 | export * from './Interpolate_unit.gen';
3 | export * from './Interpolate_transform.gen';
4 | export * from './Interpolate_box_shadow.gen';
5 |
--------------------------------------------------------------------------------
/src/parsers/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2016, React Community
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/src/parsers/Parse_box_shadow.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Parse_box_shadow.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Parse_box_shadowBS = require('./Parse_box_shadow.bs');
7 |
8 | // tslint:disable-next-line:interface-over-type-literal
9 | export type cssBoxShadow = {
10 | readonly offsetX: string;
11 | readonly offsetY: string;
12 | readonly blur: string;
13 | readonly spread: string;
14 | readonly color: string;
15 | readonly inset: boolean
16 | };
17 |
18 | export const testBoxShadow: (_1:string) => boolean = Parse_box_shadowBS.testBoxShadow;
19 |
20 | export const testBoxShadows: (_1:string) => boolean = Parse_box_shadowBS.testBoxShadows;
21 |
22 | export const parseBoxShadow: (_1:string) => cssBoxShadow = Parse_box_shadowBS.parseBoxShadow;
23 |
24 | export const parseBoxShadows: (_1:string) => string[] = Parse_box_shadowBS.parseBoxShadows;
25 |
--------------------------------------------------------------------------------
/src/parsers/Parse_box_shadow.res:
--------------------------------------------------------------------------------
1 | @bs.module("./normalize-color")
2 | external normalizeColor: string => Js.Nullable.t = "normalizeColor"
3 |
4 | let outerWhitespaceRe = %re("/(?!\(.*)\s(?![^(]*?\))/g")
5 | let outerCommaRe = %re("/(?!\(.*),(?![^(]*?\))/g")
6 |
7 | let testBoxShadow = val_ =>
8 | switch val_ {
9 | | "none" => true
10 | | _ =>
11 | let properties = Js.String.splitByRe(outerWhitespaceRe, val_) |> Js.Array.filter(str =>
12 | switch str {
13 | | Some(s) => s !== "inset" && normalizeColor(s) === Js.Nullable.null
14 | | None => false
15 | }
16 | )
17 |
18 | switch Array.length(properties) {
19 | | n when n >= 2 && n <= 4 =>
20 | properties |> Js.Array.every(p => p->Belt.Option.getWithDefault("") |> Parse_unit.testUnit)
21 | | _ => false
22 | }
23 | }
24 |
25 | let testBoxShadows = val_ => {
26 | let boxShadows = Js.String.splitByRe(outerCommaRe, val_)
27 |
28 | boxShadows |> Js.Array.every(boxShadow =>
29 | switch boxShadow {
30 | | Some(bs) => testBoxShadow(Js.String.trim(bs))
31 | | None => false
32 | }
33 | )
34 | }
35 |
36 | type cssBoxShadow = {
37 | offsetX: string,
38 | offsetY: string,
39 | blur: string,
40 | spread: string,
41 | color: string,
42 | inset: bool,
43 | }
44 |
45 | let none = {
46 | offsetX: "0",
47 | offsetY: "0",
48 | blur: "0",
49 | spread: "0",
50 | color: "rgba(0, 0, 0, 1)",
51 | inset: false,
52 | }
53 |
54 | let parseBoxShadow = val_ => {
55 | let properties = Js.String.splitByRe(outerWhitespaceRe, val_)
56 |
57 | let inset = ref(false)
58 | let color = ref("rgba(0, 0, 0, 1)")
59 |
60 | let filteredProperties = properties |> Js.Array.filter(s =>
61 | switch s {
62 | | Some(s) when s === "inset" =>
63 | inset := true
64 | false
65 | | Some(s) when normalizeColor(s) !== Js.Nullable.null =>
66 | color := s
67 | false
68 | | Some(_) => true
69 | | None => false
70 | }
71 | ) |> Array.map(s => s->Belt.Option.getWithDefault(""))
72 |
73 | switch Array.length(filteredProperties) {
74 | | n when n >= 2 && n <= 4 => {
75 | offsetX: filteredProperties[0],
76 | offsetY: filteredProperties[1],
77 | blur: Belt.Array.get(filteredProperties, 2)->Belt.Option.getWithDefault("0px"),
78 | spread: Belt.Array.get(filteredProperties, 3)->Belt.Option.getWithDefault("0px"),
79 | color: color.contents,
80 | inset: inset.contents,
81 | }
82 | | _ => none
83 | }
84 | }
85 |
86 | let parseBoxShadows = val_ => {
87 | let boxShadows = Js.String.splitByRe(outerCommaRe, val_)
88 |
89 | boxShadows |> Array.map(boxShadow =>
90 | switch boxShadow {
91 | | Some(bs) => Js.String.trim(bs)
92 | | None => ""
93 | }
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/src/parsers/Parse_box_shadow.resi:
--------------------------------------------------------------------------------
1 | export testBoxShadow: string => bool
2 |
3 | export testBoxShadows: string => bool
4 |
5 | type cssBoxShadow = {
6 | offsetX: string,
7 | offsetY: string,
8 | blur: string,
9 | spread: string,
10 | color: string,
11 | inset: bool,
12 | }
13 |
14 | export parseBoxShadow: string => cssBoxShadow
15 |
16 | export parseBoxShadows: string => array
17 |
--------------------------------------------------------------------------------
/src/parsers/Parse_color.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 | import * as NormalizeColor from "./normalize-color";
4 |
5 | function testColor(val_) {
6 | return NormalizeColor.normalizeColor(val_) !== null;
7 | }
8 |
9 | function parseColor(val_) {
10 | return NormalizeColor.rgba(val_);
11 | }
12 |
13 | export {
14 | testColor ,
15 | parseColor ,
16 |
17 | }
18 | /* ./normalize-color Not a pure module */
19 |
--------------------------------------------------------------------------------
/src/parsers/Parse_color.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Parse_color.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Parse_colorBS = require('./Parse_color.bs');
7 |
8 | // tslint:disable-next-line:interface-over-type-literal
9 | export type rgba = {
10 | readonly r: number;
11 | readonly g: number;
12 | readonly b: number;
13 | readonly a: number
14 | };
15 |
16 | export const testColor: (_1:string) => boolean = Parse_colorBS.testColor;
17 |
18 | export const parseColor: (_1:number) => rgba = Parse_colorBS.parseColor;
19 |
--------------------------------------------------------------------------------
/src/parsers/Parse_color.res:
--------------------------------------------------------------------------------
1 | @bs.module("./normalize-color")
2 | external normalizeColor: string => Js.Nullable.t = "normalizeColor"
3 |
4 | type rgba = {
5 | r: float,
6 | g: float,
7 | b: float,
8 | a: float,
9 | }
10 |
11 | @bs.module("./normalize-color") external rgba: float => rgba = "rgba"
12 |
13 | let testColor = val_ => normalizeColor(val_) !== Js.Nullable.null
14 |
15 | let parseColor = val_ => rgba(val_)
16 |
--------------------------------------------------------------------------------
/src/parsers/Parse_color.resi:
--------------------------------------------------------------------------------
1 | export type rgba = {
2 | r: float,
3 | g: float,
4 | b: float,
5 | a: float,
6 | }
7 |
8 | export testColor: string => bool
9 |
10 | export parseColor: float => rgba
11 |
--------------------------------------------------------------------------------
/src/parsers/Parse_number.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 |
4 | var onlyNumericRe = /[\d.]/g;
5 |
6 | function testNumber(val_) {
7 | if (isNaN(parseFloat(val_))) {
8 | return false;
9 | } else {
10 | return onlyNumericRe.test(val_);
11 | }
12 | }
13 |
14 | function parseNumber(val_) {
15 | return parseFloat(val_);
16 | }
17 |
18 | export {
19 | testNumber ,
20 | parseNumber ,
21 |
22 | }
23 | /* No side effect */
24 |
--------------------------------------------------------------------------------
/src/parsers/Parse_number.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Parse_number.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Parse_numberBS = require('./Parse_number.bs');
7 |
8 | export const testNumber: (_1:string) => boolean = Parse_numberBS.testNumber;
9 |
10 | export const parseNumber: (_1:string) => number = Parse_numberBS.parseNumber;
11 |
--------------------------------------------------------------------------------
/src/parsers/Parse_number.res:
--------------------------------------------------------------------------------
1 | @bs.val external parseFloat: string => float = "parseFloat"
2 |
3 | let onlyNumericRe = %re("/[\d.]/g")
4 |
5 | let testNumber = val_ => !Js.Float.isNaN(parseFloat(val_)) && Js.Re.test_(onlyNumericRe, val_)
6 |
7 | let parseNumber = val_ => parseFloat(val_)
8 |
--------------------------------------------------------------------------------
/src/parsers/Parse_number.resi:
--------------------------------------------------------------------------------
1 | export testNumber: string => bool
2 |
3 | export parseNumber: string => float
4 |
--------------------------------------------------------------------------------
/src/parsers/Parse_transform.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Parse_transform.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Curry = require('bs-platform/lib/es6/curry.js');
7 |
8 | // tslint:disable-next-line:no-var-requires
9 | const Parse_transformBS = require('./Parse_transform.bs');
10 |
11 | // tslint:disable-next-line:interface-over-type-literal
12 | export type cssTransform = { readonly transform: (null | undefined | string); readonly transformProperty: (null | undefined | string) };
13 |
14 | export const testTransform: (_1:string) => boolean = Parse_transformBS.testTransform;
15 |
16 | export const testTransforms: (_1:string) => boolean = Parse_transformBS.testTransforms;
17 |
18 | export const parseTransform: (_1:string) => cssTransform = Parse_transformBS.parseTransform;
19 |
20 | export const parseTransforms: (_1:string) => string[] = Parse_transformBS.parseTransforms;
21 |
22 | export const getAnimatableNoneForTransform: (_1:string, _2:string) => string = function (Arg1: any, Arg2: any) {
23 | const result = Curry._2(Parse_transformBS.getAnimatableNoneForTransform, Arg1, Arg2);
24 | return result
25 | };
26 |
--------------------------------------------------------------------------------
/src/parsers/Parse_transform.resi:
--------------------------------------------------------------------------------
1 | @bs.deriving(jsConverter)
2 | type transformProperties = [
3 | | #translate
4 | | #translateX
5 | | #translateY
6 | | #translateZ
7 | | #translate3d
8 | | #skew
9 | | #skewX
10 | | #skewY
11 | | #rotate
12 | | #rotateX
13 | | #rotateY
14 | | #rotateZ
15 | | #rotate3d
16 | | #scale
17 | | #scaleX
18 | | #scaleY
19 | | #scaleZ
20 | | #scale3d
21 | | #perspective
22 | | #matrix
23 | | #matrix3d
24 | ]
25 |
26 | export testTransform: string => bool
27 |
28 | export testTransforms: string => bool
29 |
30 | type cssTransform = {
31 | transform: Js.Nullable.t,
32 | transformProperty: Js.Nullable.t,
33 | }
34 |
35 | export parseTransform: string => cssTransform
36 |
37 | export parseTransforms: string => array
38 |
39 | export getAnimatableNoneForTransform: (string, string) => string
40 |
--------------------------------------------------------------------------------
/src/parsers/Parse_unit.bs.js:
--------------------------------------------------------------------------------
1 | // Generated by ReScript, PLEASE EDIT WITH CARE
2 |
3 |
4 | var _map = {"px":"px","em":"em","rem":"rem","ch":"ch","vw":"vw","vh":"vh","vmin":"vmin","vmax":"vmax","pct":"%","deg":"deg","rad":"rad","turn":"turn"};
5 |
6 | var _revMap = {"px":"px","em":"em","rem":"rem","ch":"ch","vw":"vw","vh":"vh","vmin":"vmin","vmax":"vmax","%":"pct","deg":"deg","rad":"rad","turn":"turn"};
7 |
8 | var numericRe = /[\d.-]+/;
9 |
10 | function decomposeUnit(val_) {
11 | var value = parseFloat(val_);
12 | var unit = _revMap[val_.replace(numericRe, "")];
13 | return {
14 | value: value,
15 | unit: unit
16 | };
17 | }
18 |
19 | function testUnit(val_) {
20 | var match = decomposeUnit(val_);
21 | var unit = match.unit;
22 | if (unit !== undefined && !isNaN(match.value)) {
23 | return val_.endsWith(_map[unit]);
24 | } else {
25 | return false;
26 | }
27 | }
28 |
29 | function parseUnit(val_) {
30 | var match = decomposeUnit(val_);
31 | var unit = match.unit;
32 | var value = match.value;
33 | if (unit !== undefined) {
34 | return {
35 | value: value,
36 | unit: _map[unit]
37 | };
38 | } else {
39 | return {
40 | value: value,
41 | unit: null
42 | };
43 | }
44 | }
45 |
46 | export {
47 | testUnit ,
48 | parseUnit ,
49 |
50 | }
51 | /* No side effect */
52 |
--------------------------------------------------------------------------------
/src/parsers/Parse_unit.gen.tsx:
--------------------------------------------------------------------------------
1 | /* TypeScript file generated from Parse_unit.resi by genType. */
2 | /* eslint-disable import/first */
3 |
4 |
5 | // tslint:disable-next-line:no-var-requires
6 | const Parse_unitBS = require('./Parse_unit.bs');
7 |
8 | // tslint:disable-next-line:interface-over-type-literal
9 | export type cssUnit = { readonly value: number; readonly unit: (null | undefined | string) };
10 |
11 | export const testUnit: (_1:string) => boolean = Parse_unitBS.testUnit;
12 |
13 | export const parseUnit: (_1:string) => cssUnit = Parse_unitBS.parseUnit;
14 |
--------------------------------------------------------------------------------
/src/parsers/Parse_unit.res:
--------------------------------------------------------------------------------
1 | @bs.deriving(jsConverter)
2 | type measurement = [#px | #em | #rem | #ch | #vw | #vh | #vmin | #vmax | @bs.as("%") #pct | #deg | #rad | #turn]
3 |
4 | type cssUnitRe = {
5 | value: float,
6 | unit: option,
7 | }
8 |
9 | type cssUnit = {
10 | value: float,
11 | unit: Js.Nullable.t,
12 | }
13 |
14 | let numericRe = %re("/[\d.-]+/")
15 | @bs.val external parseFloat: string => float = "parseFloat"
16 |
17 | let decomposeUnit = (val_: string): cssUnitRe => {
18 | let value = parseFloat(val_)
19 | let unit = Js.String.replaceByRe(numericRe, "", val_)->measurementFromJs
20 |
21 | {value: value, unit: unit}
22 | }
23 |
24 | let testUnit = val_ => {
25 | let {value, unit}: cssUnitRe = decomposeUnit(val_)
26 |
27 | switch (value, unit) {
28 | | (v, Some(u)) => !Js.Float.isNaN(v) && Js.String.endsWith(u->measurementToJs, val_)
29 | | (_, None) => false
30 | }
31 | }
32 |
33 | let parseUnit = val_ => {
34 | let {value, unit}: cssUnitRe = decomposeUnit(val_)
35 |
36 | switch unit {
37 | | Some(u) => {value: value, unit: Js.Nullable.return(u->measurementToJs)}
38 | | None => {value: value, unit: Js.Nullable.null}
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/parsers/Parse_unit.resi:
--------------------------------------------------------------------------------
1 | type cssUnit = {
2 | value: float,
3 | unit: Js.Nullable.t,
4 | }
5 |
6 | export testUnit: string => bool
7 |
8 | export parseUnit: string => cssUnit
9 |
--------------------------------------------------------------------------------
/src/parsers/derive-style.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 |
3 | import { deriveTransforms } from './derive-transform';
4 | import { CSSPairs } from './pairs';
5 |
6 | export const deriveStyle = (
7 | element: HTMLElement | SVGElement,
8 | to: CSSProperties
9 | ): CSSPairs => {
10 | const currentStyle = element.style;
11 | const computedStyle = window.getComputedStyle(element);
12 |
13 | return Object.entries(to).reduce(
14 | (acc, [property, value]) => {
15 | const currentValue = currentStyle.getPropertyValue(property);
16 |
17 | // The computed value of transform is always returned as a matrix.
18 | // To prevent having to reverse parse the matrix, we build a transform
19 | // string from the sparse set of transforms present on style, if any.
20 | if (property === 'transform') {
21 | const { from: fromTransform, to: toTransform } = deriveTransforms(
22 | currentValue,
23 | to[property] ?? ''
24 | );
25 |
26 | return {
27 | from: {
28 | ...acc.from,
29 | [property]: fromTransform,
30 | },
31 | to: {
32 | ...acc.to,
33 | [property]: toTransform,
34 | },
35 | };
36 | }
37 |
38 | const fromValue = currentValue
39 | ? currentValue
40 | : computedStyle.getPropertyValue(property);
41 |
42 | const from =
43 | typeof to[property as keyof CSSProperties] === 'number'
44 | ? parseFloat(fromValue)
45 | : fromValue;
46 |
47 | return {
48 | from: {
49 | ...acc.from,
50 | [property]: from,
51 | },
52 | to: {
53 | ...acc.to,
54 | [property]: value,
55 | },
56 | };
57 | },
58 | { from: {}, to: {} }
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/parsers/derive-transform.ts:
--------------------------------------------------------------------------------
1 | import {
2 | parseTransform,
3 | parseTransforms,
4 | getAnimatableNoneForTransform,
5 | } from './Parse_transform.gen';
6 |
7 | const formatTransformString = (transformRecord: Record) =>
8 | Object.entries(transformRecord)
9 | .map(([property, value]) => `${property}(${value})`)
10 | .join(' ');
11 |
12 | export const deriveTransforms = (
13 | currentTransform: string,
14 | targetTransform: string
15 | ): { from: string; to: string } => {
16 | const [fromTransform, toTransform] = [currentTransform, targetTransform].map(
17 | (transform) =>
18 | parseTransforms(transform).reduce>((acc, t) => {
19 | const { transformProperty, transform } = parseTransform(t);
20 |
21 | if (transformProperty && transform) {
22 | return {
23 | ...acc,
24 | [transformProperty]: transform,
25 | };
26 | }
27 |
28 | return acc;
29 | }, {})
30 | );
31 |
32 | Object.entries(fromTransform).forEach(([property, value]) => {
33 | // If toTransform doesn't have a matching transfrom, ensure an animatable none gets added to it.
34 | if (!toTransform[property]) {
35 | toTransform[property] = getAnimatableNoneForTransform(property, value);
36 | }
37 | });
38 |
39 | Object.entries(toTransform).forEach(([property, value]) => {
40 | // If fromTransform doesn't have a matching transform, ensure an animatable none gets added to it.
41 | if (!fromTransform[property]) {
42 | fromTransform[property] = getAnimatableNoneForTransform(property, value);
43 | }
44 | });
45 |
46 | return {
47 | from: formatTransformString(fromTransform),
48 | to: formatTransformString(toTransform),
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/src/parsers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Parse_color.gen';
2 | export * from './Parse_number.gen';
3 | export * from './Parse_transform.gen';
4 | export * from './Parse_box_shadow.gen';
5 | export * from './Parse_unit.gen';
6 | export * from './pairs';
7 | export * from './derive-style';
8 |
--------------------------------------------------------------------------------
/src/rAF/index.ts:
--------------------------------------------------------------------------------
1 | export * from './rAF';
2 | export * from './update';
3 |
--------------------------------------------------------------------------------
/src/rAF/rAF.ts:
--------------------------------------------------------------------------------
1 | import { Listener } from '../animation';
2 |
3 | interface RAFState {
4 | lastFrame: DOMHighResTimeStamp;
5 | animationFrameId: number | null;
6 | listener: Listener;
7 | }
8 |
9 | interface RAF {
10 | start: (listener: Listener) => void;
11 | stop: () => void;
12 | }
13 |
14 | export const rAF = (): RAF => {
15 | const state: RAFState = {
16 | lastFrame: typeof window !== 'undefined' ? performance.now() : Date.now(),
17 | animationFrameId: null,
18 | listener: ((() => {}) as unknown) as RAFState['listener'],
19 | };
20 |
21 | const stop = () => {
22 | state.animationFrameId !== null &&
23 | cancelAnimationFrame(state.animationFrameId);
24 | };
25 |
26 | const draw = (timestamp: DOMHighResTimeStamp) => {
27 | state.animationFrameId = requestAnimationFrame((timestamp) => {
28 | draw(timestamp);
29 | });
30 |
31 | state.listener(timestamp, state.lastFrame, stop);
32 | state.lastFrame = timestamp;
33 | };
34 |
35 | const start = (listener: RAFState['listener']) => {
36 | state.listener = listener;
37 | draw(state.lastFrame);
38 | };
39 |
40 | return {
41 | start,
42 | stop,
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/src/rAF/update.ts:
--------------------------------------------------------------------------------
1 | import { StatefulAnimatingElement, AnimationCallbacks } from '../animation';
2 |
3 | interface UpdateParams extends AnimationCallbacks {
4 | animatingElements: Set>;
5 | }
6 |
7 | /**
8 | * The core updater function ran inside requestAnimationFrame.
9 | * Iterates through all animating elements in the Set and updates
10 | * their local animation state.
11 | */
12 | export function update({
13 | animatingElements,
14 | checkReversePlayState,
15 | applyForceForStep,
16 | checkStoppingCondition,
17 | }: UpdateParams) {
18 | return function loop(
19 | timestamp: DOMHighResTimeStamp,
20 | lastFrame: number,
21 | stop: () => void
22 | ): void {
23 | // Obtain the timestamp of the last frame.
24 | // If this is the first frame, use the current frame timestamp.
25 | let lastTime = lastFrame !== undefined ? lastFrame : timestamp;
26 |
27 | // If more than four frames have been dropped since the last frame,
28 | // just use the current frame timestamp.
29 | if (timestamp > lastTime + 64) {
30 | lastTime = timestamp;
31 | }
32 |
33 | // Determine the number of steps between the current frame and last recorded frame.
34 | const steps = Math.floor(timestamp - lastTime);
35 |
36 | // Iterate through the nodes.
37 | for (const element of animatingElements) {
38 | // If the element has finished animating, is paused, or is delayed, skip it.
39 | if (
40 | element.state.complete ||
41 | element.state.paused ||
42 | element.state.delayed
43 | ) {
44 | continue;
45 | }
46 |
47 | for (let i = 0; i < steps; i++) {
48 | // If the element is configured to repeat its animation...
49 | if (typeof element.repeat === 'number' && element.repeat >= 0) {
50 | // Check if we've reached the reversal condition for repeated animations.
51 | checkReversePlayState(element);
52 | }
53 |
54 | element.state.mover = applyForceForStep(element);
55 | }
56 |
57 | const shouldComplete =
58 | (typeof element.repeat !== 'number' || element.repeat <= 0) &&
59 | checkStoppingCondition(element);
60 | const repetitionsEclipsed = element.repeat === element.state.repeatCount;
61 |
62 | if (shouldComplete) {
63 | element.onComplete();
64 | element.state.complete = true;
65 | } else if (repetitionsEclipsed) {
66 | element.onComplete(element.state.playState);
67 | element.state.complete = true;
68 | } else {
69 | element.onUpdate({
70 | velocity: element.state.mover.velocity,
71 | position: element.state.mover.position,
72 | acceleration: element.state.mover.acceleration,
73 | });
74 | }
75 | }
76 |
77 | const allAnimationsComplete =
78 | animatingElements.size > 0 &&
79 | Array.from(animatingElements).every((element) => element.state.complete);
80 |
81 | if (allAnimationsComplete) {
82 | stop();
83 | }
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/stories/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, MouseEvent } from 'react';
2 |
3 | import './button.css';
4 |
5 | interface Props {
6 | onClick: (ev: MouseEvent) => void;
7 | }
8 |
9 | const Button: FC = ({ onClick, children }) => (
10 |
13 | );
14 |
15 | export default Button;
16 |
--------------------------------------------------------------------------------
/stories/components/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, FC } from 'react';
2 |
3 | import './toggle.css';
4 |
5 | interface Props {
6 | on: boolean;
7 | onChange: (ev: ChangeEvent) => void;
8 | }
9 |
10 | const Toggle: FC = ({ onChange, on }) => (
11 |
12 |
19 |
20 |
21 | );
22 |
23 | export default Toggle;
24 |
--------------------------------------------------------------------------------
/stories/components/button.css:
--------------------------------------------------------------------------------
1 | button {
2 | display: inline-block;
3 | border: none;
4 | padding: 1rem 2rem;
5 | margin: 0;
6 | text-decoration: none;
7 | cursor: pointer;
8 | }
9 |
10 | .button {
11 | position: relative;
12 | background: var(--color-space-black);
13 | color: rgba(255, 255, 255, 0.8);
14 | box-shadow: 0px 10px 30px 5px rgba(0, 0, 0, 0.2);
15 | font-size: 1em;
16 | font-weight: 700;
17 | border-radius: 5px;
18 | transition: transform 0.2s ease;
19 | }
20 |
21 | .button:hover {
22 | transform: scale(1.1);
23 | }
24 |
25 | .button-container {
26 | display: flex;
27 | flex-direction: column;
28 | margin: 2rem;
29 | }
30 |
31 | .button-container > * {
32 | margin-top: 0;
33 | margin-bottom: 0;
34 | }
35 |
36 | .button-container > * + * {
37 | margin-top: 1rem;
38 | }
39 |
--------------------------------------------------------------------------------
/stories/components/toggle.css:
--------------------------------------------------------------------------------
1 | .toggle {
2 | position: relative;
3 | width: 55px;
4 | margin: 20px;
5 | }
6 |
7 | .toggle input {
8 | opacity: 0;
9 | position: absolute;
10 | }
11 |
12 | .toggle input + label {
13 | position: relative;
14 | display: inline-block;
15 | user-select: none;
16 | height: 30px;
17 | width: 50px;
18 | border: 1px solid #e4e4e4;
19 | border-radius: 60px;
20 | transition: 0.4s ease;
21 | }
22 |
23 | .toggle input + label::before {
24 | content: '';
25 | position: absolute;
26 | display: block;
27 | height: 30px;
28 | width: 51px;
29 | top: 0;
30 | left: 0;
31 | border-radius: 30px;
32 | transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1);
33 | }
34 |
35 | .toggle input + label::after {
36 | content: '';
37 | position: absolute;
38 | display: block;
39 | background: var(--color-orange);
40 | height: 28px;
41 | width: 28px;
42 | top: 1px;
43 | left: 0px;
44 | border-radius: 60px;
45 | box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 0px 0 hsla(0, 0%, 0%, 0.04),
46 | 0 4px 9px hsla(0, 0%, 0%, 0.13), 0 3px 3px hsla(0, 0%, 0%, 0.05);
47 | transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1);
48 | }
49 |
50 | .toggle input:checked + label::before {
51 | transition: width 0.2s cubic-bezier(0, 0, 0, 0.1);
52 | background: var(--color-space-black);
53 | }
54 |
55 | .toggle input:checked + label::after {
56 | left: 24px;
57 | }
58 |
--------------------------------------------------------------------------------
/stories/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | :root {
6 | --color-white: #ffffff;
7 | --color-renature: #7860ed;
8 | --color-red: #f25050;
9 | --color-magenta: #d90467;
10 | --color-orange: #f29441;
11 | --color-yellow: #f2cf63;
12 | --color-space-black: #011826;
13 | --color-space-blue: #053959;
14 | }
15 |
16 | body {
17 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir,
18 | helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
19 | }
20 |
21 | #root {
22 | position: absolute;
23 | inset: 0;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | }
28 |
29 | .mover {
30 | height: 100px;
31 | width: 100px;
32 | border-radius: 10px;
33 | transform-origin: center;
34 | }
35 |
36 | .mover--purple {
37 | background: var(--color-renature);
38 | }
39 |
40 | .mover--magenta {
41 | background: var(--color-magenta);
42 | }
43 |
44 | .mover--yellow {
45 | background: var(--color-yellow);
46 | }
47 |
48 | .mover--red {
49 | background: var(--color-red);
50 | }
51 |
52 | .space {
53 | height: 100%;
54 | width: 100%;
55 | background: linear-gradient(
56 | var(--color-space-black),
57 | var(--color-space-blue)
58 | );
59 | }
60 |
61 | .mover-2d {
62 | height: 25px;
63 | width: 25px;
64 | border-radius: 50%;
65 | background: var(--color-white);
66 | box-shadow: 0 0 8px 4px var(--color-white), 0 0 16px 8px var(--color-renature);
67 | }
68 |
69 | .attractor-2d {
70 | position: relative;
71 | height: 50px;
72 | width: 50px;
73 | border-radius: 50%;
74 | background: var(--color-yellow);
75 | transform: translate(-50%, -50%);
76 | box-shadow: 0 0 30px 15px var(--color-yellow);
77 | }
78 |
79 | .react-logo {
80 | stroke: var(--color-renature);
81 | stroke-width: 3;
82 | fill: transparent;
83 | }
84 |
85 | .stack-horizontal {
86 | display: flex;
87 | }
88 |
89 | .stack-horizontal > * {
90 | margin-left: 0;
91 | margin-right: 0;
92 | }
93 |
94 | .stack-horizontal > * + * {
95 | margin-left: 1rem;
96 | }
97 |
98 | .letter {
99 | font-weight: 700;
100 | color: var(--color-renature);
101 | font-size: 4rem;
102 | opacity: 0;
103 | }
104 |
--------------------------------------------------------------------------------
/stories/useFluidResistanceGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { withKnobs, number, boolean } from '@storybook/addon-knobs';
3 |
4 | import { useFluidResistanceGroup } from '../src';
5 |
6 | import Button from './components/Button';
7 | import { getRandomHex } from './utils';
8 |
9 | import './index.css';
10 |
11 | export default {
12 | title: 'FluidResistanceMultiple',
13 | decorators: [withKnobs],
14 | };
15 |
16 | export const FluidResistanceMultipleBasic: FC = () => {
17 | const [nodes] = useFluidResistanceGroup(5, (i) => ({
18 | from: {
19 | transform: 'translateY(0px)',
20 | background: '#7860ed',
21 | borderRadius: '10%',
22 | },
23 | to: {
24 | transform: 'translateY(100px)',
25 | background: getRandomHex(),
26 | borderRadius: `${Math.floor(Math.random() * 100)}%`,
27 | },
28 | config: {
29 | mass: number('mass', 20),
30 | rho: number('rho', 20),
31 | area: number('area', 20),
32 | cDrag: number('cDrag', 0.1),
33 | settle: boolean('settle', true),
34 | },
35 | delay: i * 500,
36 | repeat: Infinity,
37 | }));
38 |
39 | return (
40 |
41 | {nodes.map((props, i) => (
42 |
43 | ))}
44 |
45 | );
46 | };
47 |
48 | export const FluidResistanceMultipleEventBased: FC = () => {
49 | const [nodes, controller] = useFluidResistanceGroup(5, (i) => ({
50 | from: {
51 | transform: 'translateY(0px)',
52 | background: '#7860ed',
53 | borderRadius: '10%',
54 | },
55 | to: {
56 | transform: 'translateY(100px)',
57 | background: getRandomHex(),
58 | borderRadius: `${Math.floor(Math.random() * 100)}%`,
59 | },
60 | config: {
61 | mass: number('mass', 25),
62 | rho: number('rho', 10),
63 | area: number('area', 20),
64 | cDrag: number('cDrag', 0.25),
65 | settle: boolean('settle', true),
66 | },
67 | pause: true,
68 | delay: i * 1000,
69 | repeat: Infinity,
70 | }));
71 |
72 | return (
73 |
74 |
75 |
76 |
77 |
78 |
79 | {nodes.map((props, i) => (
80 |
81 | ))}
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/stories/useGravity2D.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useLayoutEffect, FC } from 'react';
2 | import { withKnobs, number } from '@storybook/addon-knobs';
3 |
4 | import { useGravity2D } from '../src';
5 | import './index.css';
6 |
7 | export default {
8 | title: 'Gravity2D',
9 | decorators: [withKnobs],
10 | };
11 |
12 | export const Gravity2DBasic: FC = () => {
13 | const [center, setCenter] = useState<[number, number]>([0, 0]);
14 |
15 | useLayoutEffect(() => {
16 | const root = document.getElementById('root');
17 |
18 | if (root) {
19 | setCenter([root.clientWidth / 2, root.clientHeight / 2]);
20 | }
21 | }, []);
22 |
23 | const [props] = useGravity2D({
24 | config: {
25 | attractorMass: number('attractorMass', 1000000000000),
26 | moverMass: number('moverMass', 10000),
27 | attractorPosition: center,
28 | initialMoverPosition: [center[0], center[1] - 200],
29 | initialMoverVelocity: [
30 | number('initialMoverVelocityX', 1),
31 | number('initialMoverVelocityY', 0),
32 | ],
33 | threshold: {
34 | min: number('thresholdMin', 20),
35 | max: number('thresholdMax', 100),
36 | },
37 | timeScale: number('timeScale', 100),
38 | },
39 | });
40 |
41 | return (
42 |
49 | );
50 | };
51 |
52 | export const Gravity2DCustomG: FC = () => {
53 | const [center, setCenter] = useState<[number, number]>([0, 0]);
54 |
55 | useLayoutEffect(() => {
56 | const root = document.getElementById('root');
57 |
58 | if (root) {
59 | setCenter([root.clientWidth / 2, root.clientHeight / 2]);
60 | }
61 | }, []);
62 |
63 | const [props] = useGravity2D({
64 | config: {
65 | attractorMass: number('attractorMass', 20),
66 | moverMass: number('moverMass', 1),
67 | attractorPosition: center,
68 | initialMoverPosition: [center[0] - 50, center[1]],
69 | initialMoverVelocity: [
70 | number('initialMoverVelocityX', 0),
71 | number('initialMoverVelocityY', 2),
72 | ],
73 | threshold: {
74 | min: number('thresholdMin', 10),
75 | max: number('thresholdMax', 25),
76 | },
77 | timeScale: number('timeScale', 100),
78 | G: number('G', 0.4),
79 | },
80 | });
81 |
82 | return (
83 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/stories/useGravityGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { withKnobs, number } from '@storybook/addon-knobs';
3 |
4 | import { useGravityGroup } from '../src';
5 |
6 | import Button from './components/Button';
7 | import { getRandomHex } from './utils';
8 |
9 | import './index.css';
10 |
11 | export default {
12 | title: 'GravityMultiple',
13 | decorators: [withKnobs],
14 | };
15 |
16 | export const GravityMultipleBasic: FC = () => {
17 | const [nodes] = useGravityGroup(5, (i) => ({
18 | from: {
19 | transform: 'translateY(0px)',
20 | background: '#7860ed',
21 | borderRadius: '10%',
22 | },
23 | to: {
24 | transform: 'translateY(100px)',
25 | background: getRandomHex(),
26 | borderRadius: `${Math.floor(Math.random() * 100)}%`,
27 | },
28 | config: {
29 | moverMass: number('moverMass', 10000),
30 | attractorMass: number('attractorMass', 1000000000000),
31 | r: number('r', 7.5),
32 | },
33 | delay: i * 500,
34 | repeat: Infinity,
35 | }));
36 |
37 | return (
38 |
39 | {nodes.map((props, i) => (
40 |
41 | ))}
42 |
43 | );
44 | };
45 |
46 | export const GravityMultipleEventBased: FC = () => {
47 | const [nodes, controller] = useGravityGroup(5, (i) => ({
48 | from: {
49 | transform: 'translateY(0px)',
50 | background: '#7860ed',
51 | borderRadius: '10%',
52 | },
53 | to: {
54 | transform: 'translateY(100px)',
55 | background: getRandomHex(),
56 | borderRadius: `${Math.floor(Math.random() * 100)}%`,
57 | },
58 | config: {
59 | moverMass: number('moverMass', 10000),
60 | attractorMass: number('attractorMass', 1000000000000),
61 | r: number('r', 7.5),
62 | },
63 | pause: true,
64 | delay: i * 1000,
65 | repeat: Infinity,
66 | }));
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 | {nodes.map((props, i) => (
76 |
77 | ))}
78 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/stories/utils/index.ts:
--------------------------------------------------------------------------------
1 | export const getRandomHex = (): string =>
2 | '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "esModuleInterop": true,
5 | "jsx": "react",
6 | "lib": ["esnext", "dom"],
7 | "noUnusedLocals": true,
8 | "rootDirs": ["./src", "./stories"],
9 | "module": "es2015",
10 | "moduleResolution": "node",
11 | "noImplicitAny": true,
12 | "noUnusedParameters": true,
13 | "pretty": true,
14 | "skipLibCheck": true,
15 | "sourceMap": true,
16 | "strict": true,
17 | "target": "esnext"
18 | },
19 | "include": [
20 | "src/**/*",
21 | "__tests__/**/*.ts",
22 | "stories/**/*.tsx",
23 | "setupTests.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------