├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── deploy-github-pages.yaml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .storybook ├── main.ts └── preview.ts ├── LICENSE ├── README.md ├── demo └── demo.gif ├── package-lock.json ├── package.json ├── src ├── drifting.component.tsx ├── drifting.hook.ts ├── drifting.interface.ts ├── drifting.story.css ├── drifting.story.tsx └── index.tsx ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 4 | "plugins": ["@typescript-eslint/eslint-plugin"], 5 | "rules": { 6 | "quotes": ["error", "single"], 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/deploy-github-pages.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - 'master' 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - id: build-publish 16 | uses: bitovi/github-actions-storybook-to-github-pages@v1.0.2 17 | with: 18 | path: storybook-static 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vscode 4 | build 5 | storybook-static 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | demo 3 | storybook-static 4 | build/drifting.story.js 5 | build/tsconfig.build.tsbuildinfo 6 | .eslintrc 7 | .storybook 8 | .github 9 | .eslintignore 10 | .editorconfig 11 | .gitignore 12 | .prettierrc 13 | tsconfig.build.json 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.story.@(ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-onboarding', 9 | '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: '@storybook/react-vite', 13 | options: {}, 14 | }, 15 | docs: { 16 | autodocs: 'tag', 17 | }, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Evgeny Zaytsev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/npm/dm/react-drifting-component.svg?style=flat-square)](https://www.npmjs.com/package/react-drifting-component) 2 | 3 | # react-drifting-component 4 | 5 | The drifting component which allows you to drift on the screen :) The component uses mousemove event for desktops and deviceorientation for mobile devices. 6 | 7 | ## Installation 8 | 9 | ``` 10 | $ npm install react-drifting-component 11 | ``` 12 | 13 | ## Demo 14 | 15 | [Try it out](https://z4o4z.github.io/react-drifting-component/) 16 | 17 | ![](./demo/demo.gif) 18 | 19 | ## Basic Usage 20 | 21 | ```js 22 | import { Drifting } from 'react-drifting-component'; 23 | 24 | // Inside of a component's render() method: 25 | render() { 26 | return ( 27 | 28 | 29 | {({ ref }) => } 30 | 31 | 32 | 33 | { 34 | ({ pos, onRef }) => 35 | 36 | } 37 | 38 | 39 | ); 40 | } 41 | ``` 42 | 43 | ## Examples 44 | 45 | Please clone the repo and run `npm run storybook` or `yarn storybook` to show examples of usages. 46 | 47 | ## Usage (API) 48 | 49 | The `Drifting` component has a few properties, as described below. 50 | 51 | > NOTE: this component uses rAF(requestAnimationFrame) if you need to support old browsers ensure that you are using polyfill for rAF! 52 | 53 | | Property | Type | Defaut | Description | 54 | | --------------------- | ---------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 55 | | `reverse` | `boolean` | `false` | Reverse drifting | 56 | | `children` | `function` | `({ pos: { x, y } }, onRef: () => ref)` | A function that gets an object with `pos` and `onRef` keys as an argument. Pos contains `x: number` and `y: number`, this numbers should pass to your component style to allow drift. `onRef` it is function which should be passed to `ref` of your component | 57 | | `maxMouseRange` | `number` | `null` | Max mouse drift range. | 58 | | `maxOrientationRange` | `number` | `maxMouseRange` | Max orientation drift range. devices. | 59 | 60 | ## Contributing 61 | 62 | I welcome contributions! Please open an issue if you have any feature ideas 63 | or find any bugs. I also accept pull requests with open arms. I will 64 | go over the issues when I have time. :) 65 | -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z4o4z/react-drifting-component/829646a812d94e4d0bf55c6542ae868436328a0d/demo/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-drifting-component", 3 | "version": "3.0.2", 4 | "description": "Drifting react component", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "clear": "rm ./build", 8 | "lint": "eslint -c ./.eslintrc .", 9 | "format": "eslint --format", 10 | "build": "tsc --build tsconfig.build.json", 11 | "storybook": "storybook dev", 12 | "build-storybook": "storybook build" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "reactjs", 17 | "javascript", 18 | "react-component", 19 | "drifting", 20 | "drifting-component", 21 | "mousemoove", 22 | "deviceorientation" 23 | ], 24 | "author": "Evgeny Zaytsev (http://github.com/z4o4z)", 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:z4o4z/react-drifting-component.git" 29 | }, 30 | "devDependencies": { 31 | "@storybook/addon-essentials": "^7.6.17", 32 | "@storybook/addon-interactions": "^7.6.17", 33 | "@storybook/addon-links": "^7.6.17", 34 | "@storybook/addon-onboarding": "^1.0.11", 35 | "@storybook/blocks": "^7.6.17", 36 | "@storybook/react": "^7.6.17", 37 | "@storybook/react-vite": "^7.6.17", 38 | "@storybook/test": "^7.6.17", 39 | "@types/react": "^18.2.57", 40 | "@types/react-dom": "^18.2.19", 41 | "@typescript-eslint/eslint-plugin": "^7.0.2", 42 | "@typescript-eslint/parser": "^7.0.2", 43 | "eslint": "^8.56.0", 44 | "eslint-config-prettier": "^9.1.0", 45 | "eslint-plugin-prettier": "^5.1.3", 46 | "eslint-plugin-storybook": "^0.8.0", 47 | "prettier": "^3.2.5", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "storybook": "^7.6.17", 51 | "typescript": "^5.3.3" 52 | }, 53 | "peerDependencies": { 54 | "react": "^16.0.0 | ^17.0.0 | ^18.0.0", 55 | "react-dom": "^16.0.0 | ^17.0.0 | ^18.0.0" 56 | }, 57 | "eslintConfig": { 58 | "extends": [ 59 | "plugin:storybook/recommended" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/drifting.component.tsx: -------------------------------------------------------------------------------- 1 | import { useDrifting } from './drifting.hook'; 2 | import { DriftingProps } from './drifting.interface'; 3 | 4 | export const Drifting = ({ reverse, children, maxMouseRange, maxOrientationRange }: DriftingProps) => { 5 | const ref = useDrifting({ 6 | reverse, 7 | maxMouseRange, 8 | maxOrientationRange, 9 | }); 10 | 11 | return children({ ref }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/drifting.hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { DriftingOptions } from './drifting.interface'; 3 | 4 | export const useDrifting = ({ reverse, maxMouseRange, maxOrientationRange = maxMouseRange }: DriftingOptions) => { 5 | const ref = useRef(null); 6 | 7 | useEffect(() => { 8 | const getDegreeSin = (degree: number) => Math.sin((Math.PI * degree) / 180); 9 | 10 | let frame: number | null; 11 | 12 | const onDeviceOrientation = ({ beta, gamma }: DeviceOrientationEvent) => { 13 | if (beta === null || gamma === null) return; 14 | 15 | frame = requestAnimationFrame(() => { 16 | if (!ref.current) return; 17 | 18 | const maxRange = maxOrientationRange * (reverse ? -1 : 1); 19 | 20 | if (window.screen.orientation.type.includes('landscape')) { 21 | ref.current.style.transform = `translateX(${getDegreeSin(beta) * maxRange}px) translateY(${getDegreeSin(gamma * 2) * maxRange}px)`; 22 | } else { 23 | ref.current.style.transform = `translateX(${getDegreeSin(gamma * 2) * maxRange}px) translateY(${getDegreeSin(beta) * maxRange}px)`; 24 | } 25 | }); 26 | }; 27 | 28 | const onMouseMove = ({ clientX, clientY }: MouseEvent) => { 29 | frame = requestAnimationFrame(() => { 30 | if (!ref.current) return; 31 | 32 | const maxRange = maxMouseRange * (reverse ? -1 : 1); 33 | 34 | const factorX = maxRange / (window.innerWidth / 2); 35 | const factorY = maxRange / (window.innerHeight / 2); 36 | 37 | const { top, left, width, height } = ref.current.getBoundingClientRect(); 38 | 39 | ref.current.style.transform = `translateX(${factorX * (clientX - left - width / 2)}px) translateY(${factorY * (clientY - top - height / 2)}px)`; 40 | }); 41 | }; 42 | 43 | window.addEventListener('mousemove', onMouseMove); 44 | window.addEventListener('deviceorientation', onDeviceOrientation); 45 | 46 | return () => { 47 | if (frame) { 48 | cancelAnimationFrame(frame); 49 | } 50 | 51 | window.removeEventListener('mousemove', onMouseMove); 52 | window.removeEventListener('deviceorientation', onDeviceOrientation); 53 | }; 54 | }, [reverse, maxMouseRange, maxOrientationRange]); 55 | 56 | return ref; 57 | }; 58 | -------------------------------------------------------------------------------- /src/drifting.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DriftingOptions { 2 | reverse?: boolean; 3 | maxMouseRange: number; 4 | maxOrientationRange?: number; 5 | } 6 | 7 | export interface DriftingRenderedProps { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | ref: React.RefObject; 10 | } 11 | 12 | export interface DriftingProps extends DriftingOptions { 13 | children: (props: DriftingRenderedProps) => React.ReactNode; 14 | } 15 | -------------------------------------------------------------------------------- /src/drifting.story.css: -------------------------------------------------------------------------------- 1 | .wrapper, 2 | .background, 3 | .text-wrapper { 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | } 10 | 11 | .background { 12 | top: -40px; 13 | left: -40px; 14 | right: -40px; 15 | bottom: -40px; 16 | background-size: cover; 17 | background-image: url(https://verdict4u.files.wordpress.com/2016/09/google-now-wallpaper-1.png); 18 | background-position: center; 19 | } 20 | 21 | .title { 22 | margin: 0; 23 | padding: 0; 24 | position: absolute; 25 | top: 50%; 26 | left: 50%; 27 | color: #fff; 28 | text-shadow: 1px 1px 1px rgb(51, 51, 51); 29 | font-family: Helvetica, Arial, sans-serif; 30 | transform: translate(-50%, -50%); 31 | } 32 | 33 | .emoji-1 { 34 | margin: 0; 35 | padding: 0; 36 | position: absolute; 37 | top: 10%; 38 | left: 10%; 39 | color: #fff; 40 | font-size: 90px; 41 | text-shadow: 1px 1px 1px rgb(51, 51, 51); 42 | font-family: Helvetica, Arial, sans-serif; 43 | } 44 | 45 | .emoji-2 { 46 | margin: 0; 47 | padding: 0; 48 | position: absolute; 49 | top: 70%; 50 | left: 70%; 51 | color: #fff; 52 | font-size: 120px; 53 | text-shadow: 1px 1px 1px rgb(51, 51, 51); 54 | font-family: Helvetica, Arial, sans-serif; 55 | transform: scaleX(-1); 56 | } 57 | 58 | .emoji-3 { 59 | margin: 0; 60 | padding: 0; 61 | position: absolute; 62 | top: 50%; 63 | left: 0%; 64 | color: #fff; 65 | font-size: 120px; 66 | text-shadow: 1px 1px 1px rgb(51, 51, 51); 67 | font-family: Helvetica, Arial, sans-serif; 68 | transform: scaleX(-1); 69 | } 70 | 71 | .emoji-4 { 72 | margin: 0; 73 | padding: 0; 74 | position: absolute; 75 | top: 0%; 76 | left: 90%; 77 | color: #fff; 78 | font-size: 100px; 79 | text-shadow: 1px 1px 1px rgb(51, 51, 51); 80 | font-family: Helvetica, Arial, sans-serif; 81 | } 82 | -------------------------------------------------------------------------------- /src/drifting.story.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryObj } from '@storybook/react'; 2 | import { DriftingRenderedProps } from './drifting.interface'; 3 | import React, { PropsWithChildren, forwardRef } from 'react'; 4 | import { Drifting } from './drifting.component'; 5 | 6 | import './drifting.story.css'; 7 | 8 | const Background = forwardRef((_, ref) => { 9 | return
; 10 | }); 11 | 12 | const Text = forwardRef( 13 | ({ text, className }, ref) => { 14 | return ( 15 |
16 |

{text}

17 |
18 | ); 19 | }, 20 | ); 21 | 22 | const meta = { 23 | title: 'Drifting', 24 | component: ({ children }: PropsWithChildren) =>
{children}
, 25 | }; 26 | 27 | export default meta; 28 | 29 | type Story = StoryObj; 30 | 31 | export const Simple: Story = { 32 | args: { 33 | children: {props => }, 34 | }, 35 | }; 36 | 37 | export const WithText: Story = { 38 | args: { 39 | children: ( 40 | <> 41 | {props => } 42 | 43 | 44 | {props => } 45 | 46 | 47 | ), 48 | }, 49 | }; 50 | 51 | export const AllInOne: Story = { 52 | args: { 53 | children: ( 54 | <> 55 | 56 | {props => } 57 | 58 | 59 | 60 | {props => } 61 | 62 | 63 | 64 | {props => } 65 | 66 | 67 | 68 | {props => } 69 | 70 | 71 | 72 | {props => } 73 | 74 | 75 | 76 | {props => } 77 | 78 | 79 | ), 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { Drifting } from './drifting.component'; 2 | export { useDrifting } from './drifting.hook'; 3 | export type { DriftingRenderedProps, DriftingOptions, DriftingProps } from './drifting.interface'; 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "module": "esnext", 6 | "jsx": "react", 7 | "noEmit": false, 8 | "composite": false, 9 | "declaration": false, 10 | "declarationMap": false, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "inlineSources": false, 14 | "isolatedModules": true, 15 | "moduleResolution": "node", 16 | "noImplicitAny": true, 17 | "preserveWatchOutput": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "allowJs": false, 21 | "resolveJsonModule": false, 22 | "incremental": true 23 | }, 24 | "exclude": ["node_modules"] 25 | } 26 | --------------------------------------------------------------------------------