├── .DS_Store ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── babel.librc ├── components ├── DemoContainer.tsx └── DemoControls.tsx ├── dist └── index.js ├── logo.png ├── murphyjs-2.4.4.tgz ├── murphyjs-2.5.0.tgz ├── murphyjs-2.5.1.tgz ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _meta.json ├── api-reference.mdx ├── examples.mdx ├── getting-started.mdx └── index.mdx ├── postcss.config.js ├── src ├── .DS_Store └── core │ ├── config.js │ └── index.js ├── static └── favicon.ico ├── styles └── globals.css ├── tailwind.config.js ├── theme.config.tsx ├── tsconfig.json ├── types └── murphy.d.ts ├── vercel.json ├── webpack.config.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | /dist 16 | 17 | # cache 18 | .cache/ 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # IDE 40 | .idea 41 | .vscode 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | .git 3 | .github 4 | .gitignore 5 | .editorconfig 6 | .prettierrc 7 | .eslintrc 8 | .eslintignore 9 | .prettierignore 10 | .vscode 11 | node_modules 12 | coverage 13 | .DS_Store 14 | 15 | # Source files 16 | src 17 | tests 18 | docs 19 | examples 20 | *.test.js 21 | *.spec.js 22 | 23 | # Build files 24 | dist/*.map 25 | *.map 26 | 27 | # Documentation 28 | *.md 29 | !README.md 30 | LICENSE 31 | 32 | # Assets 33 | logo.png 34 | *.png 35 | *.jpg 36 | *.jpeg 37 | *.gif 38 | *.svg 39 | *.ico 40 | 41 | # Config files 42 | *.config.js 43 | *.config.json 44 | tsconfig.json 45 | babel.config.js 46 | jest.config.js 47 | webpack.config.js 48 | rollup.config.js 49 | 50 | # Misc 51 | .env 52 | .env.* 53 | *.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cesar Oliveira 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 | # MurphyJS 2 | 3 |
4 | 5 | MurphyJS Logo 6 |

7 | npm version 8 | 9 | npm downloads 10 | license 11 |
12 |
13 | 14 | A lightweight JavaScript library for creating smooth animations with a simple API. 15 |
16 | 17 | 18 | ## Features 19 | 20 | - 🚀 Lightweight and fast (only 3.7KB) 21 | - 🎨 Simple and intuitive API 22 | - 🌈 Beautiful animations 23 | - 📱 Mobile-friendly 24 | - 🎯 No dependencies 25 | - 🎮 Total control of IntersectionObserver parameters 26 | - 🎁 Some animations implemented by default 27 | - 🏝 Plug and play solution to landing pages and simple projects 28 | - ❎ Native fallback to not supported browsers 29 | - 🛎️ Built-in event system for animation lifecycle (in, out, finish, cancel, reset, cleanup) 30 | - 🔄 Mirror animations for smooth scroll transitions 31 | - 📏 Viewport position control with predefined aliases 32 | - 📱 Mobile optimization with device detection 33 | 34 | ## Installation 35 | 36 | Using npm: 37 | ```bash 38 | npm install murphyjs 39 | ``` 40 | 41 | Using yarn: 42 | ```bash 43 | yarn add murphyjs 44 | ``` 45 | 46 | Using CDN: 47 | ```html 48 | 49 | 55 | ``` 56 | 57 | For detailed documentation and examples, visit our [documentation site](https://www.murphyjs.org/). 58 | 59 | ## Quick Start 60 | 61 | ```javascript 62 | import { Murphy } from 'murphyjs'; 63 | 64 | // Create a new instance 65 | const murphy = new Murphy(); 66 | 67 | // Animate elements 68 | murphy.animate('.box', { 69 | opacity: [0, 1], 70 | y: [20, 0], 71 | duration: 1000 72 | }); 73 | ``` 74 | 75 | ## Usage 76 | 77 | ### 1. Tag your HTML 78 | 79 | In your markup, decorate your element with attribute `data-murphy`: 80 | 81 | ```html 82 |
Any content here
83 | ``` 84 | 85 | The default effect of murphy is `bottom-to-top`, but you can also use: 86 | - `top-to-bottom` 87 | - `left-to-right` 88 | - `right-to-left` 89 | 90 | ### 2. Reset your CSS 91 | 92 | In your CSS, reset all the tagged elements: 93 | 94 | ```css 95 | *[data-murphy] { 96 | opacity: 0; 97 | } 98 | ``` 99 | 100 | ### 3. Start murphy 101 | 102 | In JavaScript, import and run `play` when your page is completely loaded: 103 | 104 | ```javascript 105 | import murphy from "murphyjs"; 106 | murphy.play(); 107 | ``` 108 | 109 | Or if you're using the script tag: 110 | 111 | ```html 112 | 113 | 116 | ``` 117 | 118 | ## Configuration 119 | 120 | You can configure the animation of each decorated element individually using these attributes: 121 | 122 | | Attribute | Type | Default | Description | 123 | |-----------|------|---------|-------------| 124 | | data-murphy | String | 'bottom-to-top' | Animation direction | 125 | | data-murphy-appearance-distance | Int | 50px | Distance from viewport edge to trigger animation | 126 | | data-murphy-element-distance | Int | 30px | Distance the element moves during animation | 127 | | data-murphy-ease | String | 'ease' | Animation easing function (can be a cubic-bezier) | 128 | | data-murphy-animation-delay | Int | 300ms | Delay before animation starts | 129 | | data-murphy-element-threshold | Float | 1.0 | How much of the element needs to be visible to trigger (0-1) | 130 | | data-murphy-animation-duration | Int | 300ms | Duration of the animation | 131 | | data-murphy-root-margin | String | '0px 0px -50px 0px' | Custom root margin for the Intersection Observer. Use this to control when animations trigger based on viewport position. You can use predefined aliases: 'top', 'middle', 'bottom', 'quarter', 'three-quarters' | 132 | | data-murphy-group | String | undefined | Group identifier for controlling animations for specific groups of elements | 133 | | data-murphy-mirror | Boolean | false | Whether to play the animation in reverse when the element leaves the viewport | 134 | | data-murphy-disable-mobile | Boolean | false | Whether to disable animations on mobile devices (screen width <= 768px or mobile user agent) | 135 | 136 | ## Advanced Features 137 | 138 | ### Mirror Animations 139 | 140 | Enable mirror animations to create smooth transitions when elements leave the viewport: 141 | 142 | ```html 143 |
144 | This element will animate in when scrolling down and animate out when scrolling up 145 |
146 | ``` 147 | 148 | ### Viewport Position Control 149 | 150 | Control when animations trigger based on the element's position in the viewport using the `data-murphy-root-margin` attribute. For convenience, we provide several aliases: 151 | 152 | ```html 153 | 154 |
155 | This will animate when it reaches the middle of the viewport 156 |
157 | 158 | 159 |
160 | This will animate when it reaches the bottom of the viewport 161 |
162 | 163 | 164 |
165 | This will animate when it's 25% from the bottom of the viewport 166 |
167 | 168 | 169 |
170 | This will animate when it's 75% from the bottom of the viewport 171 |
172 | ``` 173 | 174 | You can also use raw CSS margin values if you need more precise control: 175 | 176 | ```html 177 |
178 | This will animate when it reaches the middle of the viewport 179 |
180 | ``` 181 | 182 | The root margin follows the CSS margin syntax: `top right bottom left`. Negative values create an inset margin, which means the animation will trigger when the element reaches that point in the viewport. 183 | 184 | ### Available Viewport Position Aliases 185 | 186 | | Alias | Description | Raw Value | 187 | |-------|-------------|-----------| 188 | | `top` | Triggers at top of viewport | `'0px 0px 0px 0px'` | 189 | | `middle` | Triggers at middle of viewport | `'0px 0px -50% 0px'` | 190 | | `bottom` | Triggers at bottom of viewport | `'0px 0px 0px 0px'` | 191 | | `quarter` | Triggers at 25% from bottom | `'0px 0px -25% 0px'` | 192 | | `three-quarters` | Triggers at 75% from bottom | `'0px 0px -75% 0px'` | 193 | 194 | ## Group-based Animations 195 | 196 | You can group elements using the `data-murphy-group` attribute. This allows you to control animations for specific groups of elements. For example, you can play or reset animations for only a subset of elements by specifying a group name: 197 | 198 | ```html 199 |
Group 1
200 |
Group 1
201 |
Group 2
202 |
Group 2
203 | ``` 204 | 205 | You can then control animations for a specific group using the API: 206 | 207 | ```js 208 | // Play animations for group1 only 209 | murphy.play('group1'); 210 | 211 | // Reset animations for group2 only 212 | murphy.reset('group2'); 213 | ``` 214 | 215 | ## API 216 | 217 | ### Global Methods 218 | 219 | | Method | Description | 220 | |--------|-------------| 221 | | `play(group?: string)` | Start monitoring elements in DOM tagged with `data-murphy` attribute. Optionally specify a group to animate only elements in that group. | 222 | | `cancel()` | Cancel all animations and reset elements to their final state. | 223 | | `reset(group?: string)` | Reset all animations to their initial state. Optionally specify a group to reset only elements in that group. | 224 | | `cleanup()` | Disconnect all Intersection Observers and clean up resources. | 225 | 226 | ### Murphy Class 227 | 228 | The `Murphy` class provides a programmatic way to create animations: 229 | 230 | ```javascript 231 | import { Murphy } from 'murphyjs'; 232 | 233 | // Create a new instance 234 | const murphy = new Murphy(); 235 | 236 | // Animate elements 237 | murphy.animate('.box', { 238 | opacity: [0, 1], 239 | y: [20, 0], 240 | duration: 1000 241 | }); 242 | ``` 243 | 244 | #### `animate(selector, options)` 245 | 246 | Animates elements matching the selector with the specified options. 247 | 248 | ##### Parameters 249 | 250 | | Parameter | Type | Description | 251 | |-----------|------|-------------| 252 | | `selector` | String | CSS selector for target elements | 253 | | `options` | Object | Animation configuration | 254 | 255 | ##### Options 256 | 257 | | Option | Type | Default | Description | 258 | |--------|------|---------|-------------| 259 | | `opacity` | Array | [0, 1] | Start and end opacity values | 260 | | `x` | Array | [0, 0] | Start and end x translation in pixels | 261 | | `y` | Array | [0, 0] | Start and end y translation in pixels | 262 | | `duration` | Number | 1000 | Animation duration in milliseconds | 263 | | `delay` | Number | 0 | Delay before animation starts in milliseconds | 264 | | `ease` | String | 'ease' | Easing function name | 265 | 266 | ### Events 267 | 268 | MurphyJS provides a set of events that you can listen to for better control and integration: 269 | 270 | | Event | Description | 271 | |-------|-------------| 272 | | `murphy:in` | Fired when an element enters the viewport | 273 | | `murphy:out` | Fired when an element leaves the viewport | 274 | | `murphy:finish` | Fired when an animation completes | 275 | | `murphy:cancel` | Fired when an animation is cancelled | 276 | | `murphy:reset` | Fired when an element is reset | 277 | | `murphy:cleanup` | Fired when observers are cleaned up | 278 | 279 | #### Event Example 280 | 281 | ```javascript 282 | document.addEventListener('murphy:in', (event) => { 283 | const { element } = event.detail; 284 | console.log('Element entered viewport:', element); 285 | }); 286 | 287 | document.addEventListener('murphy:finish', (event) => { 288 | const { element } = event.detail; 289 | console.log('Animation finished:', element); 290 | }); 291 | ``` 292 | 293 | ### Available Animations 294 | 295 | MurphyJS comes with several built-in animations that you can use with the `data-murphy` attribute: 296 | 297 | #### Basic Animations 298 | - `bottom-to-top` 299 | - `top-to-bottom` 300 | - `left-to-right` 301 | - `right-to-left` 302 | 303 | #### Flip Animations 304 | - `flip-left` 305 | - `flip-right` 306 | - `flip-up` 307 | - `flip-down` 308 | 309 | #### Zoom Animations 310 | - `zoom-in` 311 | - `zoom-out` 312 | 313 | #### Fade Animations 314 | - `fade` 315 | - `fade-up` 316 | - `fade-down` 317 | - `fade-left` 318 | - `fade-right` 319 | 320 | #### Rotate Animations 321 | - `rotate-left` 322 | - `rotate-right` 323 | 324 | #### Scale Animations 325 | - `scale-up` 326 | - `scale-down` 327 | 328 | #### Slide Animations 329 | - `slide-up` 330 | - `slide-down` 331 | - `slide-left` 332 | - `slide-right` 333 | 334 | #### Bounce Animations 335 | - `bounce-in` 336 | - `bounce-out` -------------------------------------------------------------------------------- /babel.librc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "current" 6 | } 7 | }] 8 | ] 9 | } -------------------------------------------------------------------------------- /components/DemoContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | interface DemoContainerProps { 4 | children: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export function DemoContainer({ children, className = '' }: DemoContainerProps) { 9 | useEffect(() => { 10 | if (typeof window !== 'undefined' && window.murphy) { 11 | window.murphy.play() 12 | } 13 | }, []) 14 | 15 | return ( 16 |
17 | {children} 18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /components/DemoControls.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | 3 | interface DemoControlsProps { 4 | containerId: string; 5 | isFixed?: boolean; 6 | group?: string; 7 | controlLabels?: { 8 | reset?: string; 9 | play?: string; 10 | }; 11 | } 12 | 13 | export function DemoControls({ containerId, isFixed = false, group, controlLabels }: DemoControlsProps) { 14 | const handleReset = useCallback(() => { 15 | if (typeof window !== "undefined" && window.murphy) { 16 | window.murphy.reset(group); 17 | } 18 | }, [group]); 19 | 20 | const handlePlay = useCallback(() => { 21 | if (typeof window !== "undefined" && window.murphy) { 22 | window.murphy.play(group); 23 | } 24 | }, [group]); 25 | 26 | return ( 27 |
28 | 31 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.murphy=t():e.murphy=t()}(this,(()=>(()=>{var e={853:e=>{var t=.1,r="function"==typeof Float32Array;function a(e,t){return 1-3*t+3*e}function n(e,t){return 3*t-6*e}function o(e){return 3*e}function i(e,t,r){return((a(t,r)*e+n(t,r))*e+o(t))*e}function s(e,t,r){return 3*a(t,r)*e*e+2*n(t,r)*e+o(t)}function c(e){return e}e.exports=function(e,a,n,o){if(!(0<=e&&e<=1&&0<=n&&n<=1))throw new Error("bezier x values must be in [0, 1] range");if(e===a&&n===o)return c;for(var l=r?new Float32Array(11):new Array(11),p=0;p<11;++p)l[p]=i(p*t,e,n);return function(r){return 0===r?0:1===r?1:i(function(r){for(var a=0,o=1;10!==o&&l[o]<=r;++o)a+=t;--o;var c=a+(r-l[o])/(l[o+1]-l[o])*t,p=s(c,e,n);return p>=.001?function(e,t,r,a){for(var n=0;n<4;++n){var o=s(t,r,a);if(0===o)return t;t-=(i(t,r,a)-e)/o}return t}(r,c,e,n):0===p?c:function(e,t,r,a,n){var o,s,c=0;do{(o=i(s=t+(r-t)/2,a,n)-e)>0?r=s:t=s}while(Math.abs(o)>1e-7&&++c<10);return s}(r,a,a+t,e,n)}(r),a,o)}}}},t={};function r(a){var n=t[a];if(void 0!==n)return n.exports;var o=t[a]={exports:{}};return e[a](o,o.exports,r),o.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var a={};return(()=>{"use strict";r.d(a,{default:()=>M});var e=r(853),t=r.n(e);const n={LEFT_TO_RIGHT:"left-to-right",RIGHT_TO_LEFT:"right-to-left",TOP_TO_BOTTOM:"top-to-bottom",BOTTOM_TO_TOP:"bottom-to-top",MOBILE_BREAKPOINT:768,MOBILE_REGEX:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i,FLIP_LEFT:"flip-left",FLIP_RIGHT:"flip-right",FLIP_UP:"flip-up",FLIP_DOWN:"flip-down",ZOOM_IN:"zoom-in",ZOOM_OUT:"zoom-out",FADE:"fade",FADE_UP:"fade-up",FADE_DOWN:"fade-down",FADE_LEFT:"fade-left",FADE_RIGHT:"fade-right",ROTATE_LEFT:"rotate-left",ROTATE_RIGHT:"rotate-right",SCALE_UP:"scale-up",SCALE_DOWN:"scale-down",SLIDE_UP:"slide-up",SLIDE_DOWN:"slide-down",SLIDE_LEFT:"slide-left",SLIDE_RIGHT:"slide-right",BOUNCE_IN:"bounce-in",BOUNCE_OUT:"bounce-out",MURPHY_SELECTOR:"[data-murphy]",APPEARANCE_DISTANCE_DEFAULT:100,ELEMENT_DISTANCE_DEFAULT:50,EASE_DEFAULT:"cubic-bezier(0.25, 0.1, 0.25, 1)",ANIMATION_DELAY_DEFAULT:0,THRESHOLD_DEFAULT:.1,ANIMATION_DURATION_DEFAULT:300,VIEWPORT_POSITIONS:{TOP:"0px 0px 0px 0px",MIDDLE:"0px 0px -50% 0px",BOTTOM:"0px 0px 0px 0px",QUARTER:"0px 0px -25% 0px",THREE_QUARTERS:"0px 0px -75% 0px"},ANIMATION_CONFIGS:{"flip-left":{transform:"rotateY(-90deg)",transformOrigin:"left center",opacity:0},"flip-right":{transform:"rotateY(90deg)",transformOrigin:"right center",opacity:0},"flip-up":{transform:"rotateX(-90deg)",transformOrigin:"center top",opacity:0},"flip-down":{transform:"rotateX(90deg)",transformOrigin:"center bottom",opacity:0},"zoom-in":{transform:"scale(0.5)",opacity:0},"zoom-out":{transform:"scale(1.5)",opacity:0},fade:{opacity:0},"fade-up":{transform:"translateY(20px)",opacity:0},"fade-down":{transform:"translateY(-20px)",opacity:0},"fade-left":{transform:"translateX(20px)",opacity:0},"fade-right":{transform:"translateX(-20px)",opacity:0},"rotate-left":{transform:"rotate(-180deg)",opacity:0},"rotate-right":{transform:"rotate(180deg)",opacity:0},"scale-up":{transform:"scale(0.5)",opacity:0},"scale-down":{transform:"scale(1.5)",opacity:0},"slide-up":{transform:"translateY(100%)",opacity:0},"slide-down":{transform:"translateY(-100%)",opacity:0},"slide-left":{transform:"translateX(100%)",opacity:0},"slide-right":{transform:"translateX(-100%)",opacity:0},"bounce-in":{transform:"scale(0.3) translateY(100px)",opacity:0},"bounce-out":{transform:"scale(1.2) translateY(-50px)",opacity:0},elastic:{transform:"scale(0.5) translateY(80px)",opacity:0},spring:{transform:"scale(0.8) translateY(60px)",opacity:0},smooth:[.4,0,.2,1],sharp:[.4,0,.6,1],swift:[.4,0,.2,1],"material-standard":[.4,0,.2,1],"material-decelerate":[0,0,.2,1],"material-accelerate":[.4,0,1,1]},BEZIER_EASINGS:{ease:[.25,.1,.25,1],linear:[0,0,1,1],"ease-in":[.42,0,1,1],"ease-out":[0,0,.58,1],"ease-in-out":[.42,0,.58,1],"material-standard":[.4,0,.2,1],"material-decelerate":[0,0,.2,1],"material-accelerate":[.4,0,1,1],bounce:[.68,-.6,.32,1.6],elastic:[.68,-.6,.32,1.6],smooth:[.4,0,.2,1],sharp:[.4,0,.6,1],swift:[.4,0,.2,1],spring:[.68,-.6,.32,1.6]}},{LEFT_TO_RIGHT:o,RIGHT_TO_LEFT:i,TOP_TO_BOTTOM:s,BOTTOM_TO_TOP:c,MURPHY_SELECTOR:l,APPEARANCE_DISTANCE_DEFAULT:p,ELEMENT_DISTANCE_DEFAULT:u,ANIMATION_DELAY_DEFAULT:m,THRESHOLD_DEFAULT:d,ANIMATION_DURATION_DEFAULT:f,BEZIER_EASINGS:T}=n,E=(e,t,r={})=>{const a=new CustomEvent(`murphy:${t}`,{detail:{element:e,...r}});document.dispatchEvent(a)},y=e=>{if(!b())return _();let t=l;return e&&(t+=`[data-murphy-group="${e}"]`),document.querySelectorAll(t).forEach((e=>{I(e)}))},_=()=>{document.querySelectorAll(l).forEach((e=>{e.style.opacity="1",e.style.transform="translate(0)",e.classList.add("murphy-animated"),E(e,"cancel")}))},O=e=>{let t=l;e&&(t+=`[data-murphy-group="${e}"]`),document.querySelectorAll(t).forEach((e=>{e.style.opacity="1",e.style.transform="none",e.offsetWidth;const t=e.dataset.murphy||c,r=e.dataset.murphyElementDistance||u,a=parseInt(e.dataset.murphyAnimationDuration)||f,o=x(e.dataset.murphyEase||n.EASE_DEFAULT),i=h(t,r),s=e.animate([{transform:"none",opacity:1},{transform:i,opacity:0}],{duration:a,easing:o,fill:"forwards"});e._animation=s,s.onfinish=()=>{e.classList.remove("murphy-animated"),e.classList.remove("murphy-in"),e.classList.remove("murphy-out"),delete e._animation,e._observer&&(e._observer.disconnect(),delete e._observer),E(e,"reset")}}))},A=()=>{document.querySelectorAll(l).forEach((e=>{e._observer&&(e._observer.disconnect(),delete e._observer),E(e,"cleanup")}))},I=e=>{if("true"===e.dataset.murphyDisableMobile&&(window.innerWidth<=n.MOBILE_BREAKPOINT||n.MOBILE_REGEX.test(navigator.userAgent)))return e.style.opacity="1",void(e.style.transform="none");const t=e.dataset.murphy||c,r=e.dataset.murphyAppearanceDistance||p,a=e.dataset.murphyElementDistance||u,o=e.dataset.murphyEase||"ease",i=parseInt(e.dataset.murphyAnimationDelay)||m,s=parseFloat(e.dataset.murphyElementThreshold)||d,l=parseInt(e.dataset.murphyAnimationDuration)||f;e.style.opacity="0",e.style.transform=h(t,a);let T=e.dataset.murphyRootMargin;if(T){const e=T.toUpperCase();n.VIEWPORT_POSITIONS[e]&&(T=n.VIEWPORT_POSITIONS[e])}else T=`0px 0px ${-1*r}px 0px`;D({elementOptions:{element:e,animationType:t,animationDuration:l,elementDistance:a,ease:o,delay:i,elementThreshold:s},observerOptions:{threshold:s,rootMargin:T}})},h=(e,t)=>{const r={[c]:`translateY(${t}px)`,[s]:`translateY(-${t}px)`,[o]:`translateX(-${t}px)`,[i]:`translateX(${t}px)`};return r[e]||r[c]},D=({elementOptions:e,observerOptions:t})=>{try{const r=e.element,a=e.animationType,n="true"===r.dataset.murphyMirror,o=new IntersectionObserver((()=>{let t;return(...i)=>{clearTimeout(t),t=setTimeout((()=>(t=>{t.forEach((t=>{const{intersectionRatio:i}=t,s=Number(e.elementThreshold)||0,c=.05,l=i<=c,p=i>c&&i=s-c&&!p?(L(r,{delay:e.delay,duration:e.animationDuration,easing:e.ease,distance:e.elementDistance,direction:a})(),n||o.unobserve(t.target),E(r,"in",{intersectionRatio:i})):n&&l&&!p&&(L(r,{delay:0,duration:e.animationDuration,easing:e.ease,distance:e.elementDistance,direction:a,reverse:!0})(),E(r,"out",{intersectionRatio:i}))}))})(...i)),100)}})(),t);r._observer=o,o.observe(r)}catch(t){L(element,{delay:e.delay,duration:e.animationDuration,easing:e.ease,distance:e.elementDistance,direction:animationType})(),E(element,"in",{error:"IntersectionObserver not supported"})}};function L(e,t){const{delay:r=n.ANIMATION_DELAY_DEFAULT,duration:a=n.ANIMATION_DURATION_DEFAULT,easing:o=n.EASE_DEFAULT,distance:i=n.ELEMENT_DISTANCE_DEFAULT,direction:s=n.LEFT_TO_RIGHT,reverse:c=!1}=t,l=Math.max(0,Number(a)||n.ANIMATION_DURATION_DEFAULT),p=Math.max(0,Number(r)||n.ANIMATION_DELAY_DEFAULT),u=Math.max(0,Number(i)||n.ELEMENT_DISTANCE_DEFAULT);return()=>{setTimeout((()=>{const t=x(o),r=n.ANIMATION_CONFIGS[s]||{transform:N(s,u),opacity:0},a=e.animate(c?[{transform:"none",opacity:1},r]:[r,{transform:"none",opacity:1}],{duration:l,easing:t,fill:"forwards"});e._animation=a,a.onfinish=()=>{c?e.classList.remove("murphy-animated"):e.classList.add("murphy-animated"),E(e,"finish",{reverse:c})}}),p)}}function N(e,t){switch(e){case n.LEFT_TO_RIGHT:return`translateX(-${t}px)`;case n.RIGHT_TO_LEFT:return`translateX(${t}px)`;case n.TOP_TO_BOTTOM:return`translateY(-${t}px)`;case n.BOTTOM_TO_TOP:return`translateY(${t}px)`;default:return"none"}}function x(e){if("string"==typeof e){if(e.startsWith("cubic-bezier"))return e;const t=n.BEZIER_EASINGS[e];if(t&&Array.isArray(t)&&4===t.length)return`cubic-bezier(${t.join(", ")})`}return n.EASE_DEFAULT}const b=()=>!!window.Animation&&!!(window.IntersectionObserver&&"IntersectionObserver"in window&&"IntersectionObserverEntry"in window&&"intersectionRatio"in window.IntersectionObserverEntry.prototype);class F{constructor(){b()||console.warn("MurphyJS: Your browser does not support required features")}animate(e,r){const a=document.querySelectorAll(e),{opacity:n=[0,1],x:o=[0,0],y:i=[0,0],duration:s=1e3,delay:c=0,ease:l="ease"}=r;a.forEach((e=>{e.style.opacity=n[0],e.style.transform=`translate(${o[0]}px, ${i[0]}px)`,setTimeout((()=>{const r=T[l]||t()(.4,0,.2,1),a=performance.now(),c=t=>{const l=t-a,p=Math.min(l/s,1),u=r(p),m=n[0]+(n[1]-n[0])*u,d=o[0]+(o[1]-o[0])*u,f=i[0]+(i[1]-i[0])*u;e.style.opacity=m,e.style.transform=`translate(${d}px, ${f}px)`,p<1?requestAnimationFrame(c):(e.style.opacity=n[1],e.style.transform=`translate(${o[1]}px, ${i[1]}px)`,E(e,"finish"))};requestAnimationFrame(c)}),c)}))}}"undefined"!=typeof window&&(window.murphy={play:y,cancel:_,reset:O,cleanup:A},window.Murphy=F);const M={play:y,cancel:_,reset:O,cleanup:A,Murphy:F}})(),a.default})())); -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/logo.png -------------------------------------------------------------------------------- /murphyjs-2.4.4.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/murphyjs-2.4.4.tgz -------------------------------------------------------------------------------- /murphyjs-2.5.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/murphyjs-2.5.0.tgz -------------------------------------------------------------------------------- /murphyjs-2.5.1.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/murphyjs-2.5.1.tgz -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require("nextra")({ 2 | theme: "nextra-theme-docs", 3 | themeConfig: "./theme.config.tsx", 4 | mdxOptions: { 5 | remarkPlugins: [], 6 | rehypePlugins: [], 7 | rehypePrettyCodeOptions: { 8 | theme: { 9 | name: "murphy-pink", 10 | type: "dark", 11 | colors: { 12 | "editor.background": "#2d1f1f", 13 | "editor.foreground": "#d3d3d3" 14 | }, 15 | tokenColors: [ 16 | { 17 | name: "Comments", 18 | scope: [ 19 | "comment", 20 | "punctuation.definition.comment", 21 | "comment.block.html", 22 | "comment.line.double-slash.js", 23 | "comment.block.js", 24 | "comment.block.css" 25 | ], 26 | settings: { 27 | foreground: "#666666" 28 | } 29 | }, 30 | { 31 | name: "Variables", 32 | scope: ["variable", "string constant.other.placeholder"], 33 | settings: { 34 | foreground: "#ff9696" 35 | } 36 | }, 37 | { 38 | name: "Keywords", 39 | scope: ["keyword", "storage.type", "storage.modifier"], 40 | settings: { 41 | foreground: "#ff6b6b" 42 | } 43 | }, 44 | { 45 | name: "Strings", 46 | scope: ["string", "string.quoted"], 47 | settings: { 48 | foreground: "#ff9898" 49 | } 50 | }, 51 | { 52 | name: "Functions", 53 | scope: ["entity.name.function", "support.function"], 54 | settings: { 55 | foreground: "#ff7b7b" 56 | } 57 | }, 58 | { 59 | name: "Numbers", 60 | scope: ["constant.numeric"], 61 | settings: { 62 | foreground: "#ff8b8b" 63 | } 64 | }, 65 | { 66 | name: "Operators", 67 | scope: ["keyword.operator"], 68 | settings: { 69 | foreground: "#ff5b5b" 70 | } 71 | }, 72 | { 73 | name: "Properties", 74 | scope: ["variable.other.property"], 75 | settings: { 76 | foreground: "#ffabab" 77 | } 78 | }, 79 | { 80 | name: "HTML Tags", 81 | scope: ["entity.name.tag"], 82 | settings: { 83 | foreground: "#ff4b4b" 84 | } 85 | }, 86 | { 87 | name: "CSS Properties", 88 | scope: ["support.type.property-name.css"], 89 | settings: { 90 | foreground: "#ffc6c6" 91 | } 92 | }, 93 | { 94 | name: "JavaScript Keywords", 95 | scope: ["keyword.control", "storage.type.js"], 96 | settings: { 97 | foreground: "#ff3b3b" 98 | } 99 | }, 100 | { 101 | name: "Data Murphy Base", 102 | scope: [ 103 | "entity.other.attribute-name.html", 104 | "entity.other.attribute-name.data-murphy", 105 | "entity.other.attribute-name.data-murphy.html" 106 | ], 107 | settings: { 108 | foreground: "#ffd6d6", 109 | fontStyle: "italic" 110 | } 111 | }, 112 | { 113 | name: "Data Murphy Animation", 114 | scope: [ 115 | "entity.other.attribute-name.data-murphy-animation", 116 | "entity.other.attribute-name.data-murphy-animation-delay", 117 | "entity.other.attribute-name.data-murphy-animation-duration" 118 | ], 119 | settings: { 120 | foreground: "#ffb6b6", 121 | fontStyle: "italic" 122 | } 123 | }, 124 | { 125 | name: "Data Murphy Element", 126 | scope: [ 127 | "entity.other.attribute-name.data-murphy-element", 128 | "entity.other.attribute-name.data-murphy-element-distance", 129 | "entity.other.attribute-name.data-murphy-element-threshold" 130 | ], 131 | settings: { 132 | foreground: "#ffc6c6", 133 | fontStyle: "italic" 134 | } 135 | }, 136 | { 137 | name: "Data Murphy Appearance", 138 | scope: [ 139 | "entity.other.attribute-name.data-murphy-appearance", 140 | "entity.other.attribute-name.data-murphy-appearance-distance" 141 | ], 142 | settings: { 143 | foreground: "#ffa6a6", 144 | fontStyle: "italic" 145 | } 146 | }, 147 | { 148 | name: "Data Murphy Other", 149 | scope: [ 150 | "entity.other.attribute-name.data-murphy-ease", 151 | "entity.other.attribute-name.data-murphy-root", 152 | "entity.other.attribute-name.data-murphy-root-margin", 153 | "entity.other.attribute-name.data-murphy-threshold" 154 | ], 155 | settings: { 156 | foreground: "#ff9b9b", 157 | fontStyle: "italic" 158 | } 159 | } 160 | ] 161 | } 162 | } 163 | }, 164 | defaultShowCopyCode: true, 165 | flexsearch: true, 166 | staticImage: true 167 | }); 168 | 169 | module.exports = withNextra({ 170 | images: { 171 | unoptimized: true, 172 | }, 173 | webpack: (config, { isServer }) => { 174 | if (!isServer) { 175 | // Copy the built Murphy.js files to the public directory 176 | const CopyPlugin = require('copy-webpack-plugin'); 177 | config.plugins.push( 178 | new CopyPlugin({ 179 | patterns: [ 180 | { 181 | from: 'dist', 182 | to: 'dist', 183 | noErrorOnMissing: true 184 | } 185 | ] 186 | }) 187 | ); 188 | } 189 | return config; 190 | } 191 | }); 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "murphyjs", 3 | "version": "2.5.2", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "dist", 8 | "README.md", 9 | "logo.png" 10 | ], 11 | "scripts": { 12 | "dev": "rm -rf .next && next dev", 13 | "build": "next build", 14 | "start": "next start", 15 | "lint": "next lint", 16 | "build:lib": "webpack --mode production", 17 | "build:docs": "next build", 18 | "export:lib": "npm run build:lib && npm pack" 19 | }, 20 | "dependencies": { 21 | "bezier-easing": "^2.1.0", 22 | "next": "^14.1.0", 23 | "nextra": "^4.2.17", 24 | "nextra-theme-docs": "^4.2.17", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.23.9", 30 | "@babel/preset-env": "^7.23.9", 31 | "@babel/preset-react": "^7.23.9", 32 | "@babel/preset-typescript": "^7.23.9", 33 | "@types/node": "^20.11.0", 34 | "@types/react": "^18.2.0", 35 | "@types/react-dom": "^18.2.0", 36 | "autoprefixer": "^10.4.17", 37 | "babel-loader": "^9.1.3", 38 | "copy-webpack-plugin": "^13.0.0", 39 | "css-loader": "^6.10.0", 40 | "postcss": "^8.4.35", 41 | "postcss-loader": "^8.1.0", 42 | "style-loader": "^3.3.4", 43 | "tailwindcss": "^3.4.1", 44 | "typescript": "^5.3.0", 45 | "webpack": "^5.90.1", 46 | "webpack-cli": "^5.1.4", 47 | "webpack-dev-server": "^4.15.1" 48 | }, 49 | "keywords": [ 50 | "animation", 51 | "scroll", 52 | "intersection-observer", 53 | "web-animations", 54 | "reveal", 55 | "fade", 56 | "transition", 57 | "javascript", 58 | "library", 59 | "frontend", 60 | "effects", 61 | "murphyjs" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from "next/app"; 2 | import React, { useEffect } from "react"; 3 | import Script from "next/script"; 4 | import Head from "next/head"; 5 | import "../styles/globals.css"; 6 | 7 | export default function App({ Component, pageProps }: AppProps) { 8 | useEffect(() => { 9 | // Import Murphy.js only on the client side 10 | if (process.env.NODE_ENV === "development") { 11 | require("../src/core/index.js"); 12 | // Call play() after the script is loaded 13 | if (typeof window !== "undefined" && window.murphy) { 14 | window.murphy.play(); 15 | } 16 | } 17 | }, []); 18 | 19 | return ( 20 | <> 21 | 22 | 26 | 27 | 28 | 21 | 27 | ``` 28 | 29 | ### Via file include 30 | 31 | Download file [here](https://www.murphyjs.org/dist/index.js) and link in your HTML: 32 | 33 | ```html copy 34 | 35 | ``` 36 | 37 | ## Basic Setup 38 | 39 | ### 1. Tag your HTML 40 | 41 | In your markup, decorate your element with attribute `data-murphy`: 42 | 43 | ```html copy 44 |
Any content here
45 | ``` 46 | 47 | The default effect of murphy.js is `bottom-to-top`, but it's possible to use: 48 | - `top-to-bottom` 49 | - `left-to-right` 50 | - `right-to-left` 51 | 52 | ### 2. Reset your CSS (Optional) 53 | 54 | If you want to prevent elements from being visible before their animation starts, you can add this CSS: 55 | 56 | ```css copy 57 | *[data-murphy] { 58 | opacity: 0; 59 | } 60 | ``` 61 | 62 | > Note: This step is optional. The library will work without it, and your elements will be visible immediately before their animation starts. This can provide a better user experience by avoiding the initial "invisible" state. 63 | 64 | ### 3. Start murphy.js 65 | 66 | #### Import 67 | 68 | ```javascript copy 69 | import murphy from "murphyjs"; 70 | ``` 71 | 72 | #### And trigger 73 | 74 | ```javascript copy 75 | murphy.play() 76 | ``` 77 | 78 | #### Or call from window 79 | 80 | If you added murphy.js via **file include**, just access murphy.js's functions in window: 81 | 82 | ```javascript copy 83 | window.murphy.play() 84 | // or just 85 | murphy.play() 86 | ``` 87 | 88 | ### Class-based Usage 89 | 90 | You can also use MurphyJS with a class-based approach for more programmatic control: 91 | 92 | ```javascript copy 93 | // Create a new instance 94 | const murphy = new Murphy(); 95 | 96 | // Animate elements 97 | murphy.animate('.box', { 98 | opacity: [0, 1], 99 | y: [20, 0], 100 | duration: 1000 101 | }); 102 | ``` 103 | 104 | The `animate` method accepts the following options: 105 | 106 | | Option | Type | Default | Description | 107 | |--------|------|---------|-------------| 108 | | opacity | Array | [0, 1] | Start and end opacity values | 109 | | x | Array | [0, 0] | Start and end x translation in pixels | 110 | | y | Array | [0, 0] | Start and end y translation in pixels | 111 | | duration | Number | 1000 | Animation duration in milliseconds | 112 | | delay | Number | 0 | Delay before animation starts in milliseconds | 113 | | ease | String | 'ease' | Easing function name | 114 | 115 | ## Available Animations 116 | 117 | ### Basic Animations 118 | 119 | ```html copy 120 |

Bottom to top

121 |

Top to bottom

122 |

Left to right

123 |

Right to left

124 | ``` 125 | 126 | ### Sequential Animations 127 | 128 | To create sequential animations like murphy.js's logo: 129 | 130 | ```html copy 131 |

m

132 |

u

133 |

r

134 |

p

135 |

h

136 |

y

137 |

.

138 |

j

139 |

s

140 | ``` 141 | 142 | ## Group-based Animations 143 | 144 | You can group elements using the `data-murphy-group` attribute. This allows you to control animations for specific groups of elements. 145 | 146 | ```html copy 147 |
Group 1
148 |
Group 1
149 |
Group 2
150 |
Group 2
151 | ``` 152 | 153 | ## Configuration Options 154 | 155 | You can configure the animation of each decorated element individually using these attributes: 156 | 157 | | Attribute | Value type | Default value | What controls | 158 | |-----------|------------|---------------|---------------| 159 | | data-murphy | String | 'bottom-to-top' | Animation direction | 160 | | data-murphy-appearance-distance | Int | 50 *(px)* | Distance from viewport edge to trigger animation | 161 | | data-murphy-element-distance | Int | 30 *(px)* | Distance the element moves during animation | 162 | | data-murphy-ease | String | 'ease' | Define a função de easing da animação. Opções: 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'cubic-in', 'cubic-out', 'cubic-in-out', 'quad-in', 'quad-out', 'quad-in-out' | 163 | | data-murphy-animation-delay | Int | 300 *(ms)* | Delay before animation starts | 164 | | data-murphy-element-threshold | Float | 1.0 | How much of the element needs to be visible to trigger (0-1) | 165 | | data-murphy-animation-duration | Int | 300 *(ms)* | Duration of the animation | 166 | | data-murphy-root-margin | String | '0px 0px -50px 0px' | Custom root margin for the Intersection Observer. Use this to control when animations trigger based on viewport position. You can use predefined aliases: 'top', 'middle', 'bottom', 'quarter', 'three-quarters' | 167 | | data-murphy-mirror | Boolean | false | Whether to play the animation in reverse when the element leaves the viewport | 168 | 169 | ### Viewport Position Control 170 | 171 | You can control when animations trigger based on the element's position in the viewport using the `data-murphy-root-margin` attribute. For convenience, we provide several aliases: 172 | 173 | ```html 174 | 175 |
176 | This will animate when it reaches the middle of the viewport 177 |
178 | 179 | 180 |
181 | This will animate when it reaches the bottom of the viewport 182 |
183 | 184 | 185 |
186 | This will animate when it's 25% from the bottom of the viewport 187 |
188 | 189 | 190 |
191 | This will animate when it's 75% from the bottom of the viewport 192 |
193 | ``` 194 | 195 | You can also use raw CSS margin values if you need more precise control: 196 | 197 | ```html 198 |
199 | This will animate when it reaches the middle of the viewport 200 |
201 | ``` 202 | 203 | The root margin follows the CSS margin syntax: `top right bottom left`. Negative values create an inset margin, which means the animation will trigger when the element reaches that point in the viewport. 204 | 205 | ### Methods 206 | 207 | | Method | What happens | 208 | |--------|--------------| 209 | | play | Start monitoring elements in DOM tagged with `data-murphy` | 210 | | reset | Resets all data-murphy elements to their initial state | 211 | | cleanup | Disconnects all Intersection Observers and cleans up resources | 212 | 213 | ## Advanced: Mirror Animations 214 | 215 | You can enable mirror animations to create smooth transitions when elements leave the viewport: 216 | 217 | ```html copy 218 |
219 | This element will animate in when scrolling down and animate out when scrolling up 220 |
221 | ``` 222 | 223 | ## Browser Support 224 | 225 | | Chrome | Safari | IE / Edge | Firefox | Opera | 226 | |--------|--------|-----------|---------|-------| 227 | | 58+ | 12.1+ | Not *(yet)* supported | 55+ | 62+ | 228 | 229 | ## Important Note 230 | 231 |
232 | 👋 Just a quick tip: While these animations are triggered by scrolling, they also work great for elements that are already visible on the page. Think of it like having those cool entrance animations from React Transition Group, but without the extra setup! Perfect for spicing up your landing pages and first-time visits. Give it a try! ✨ 233 |
234 | 235 | ## Advanced: Listening to Animation Events 236 | 237 | murphy.js emits custom events for each animation lifecycle. You can listen to these events for advanced integrations: 238 | 239 | | Event | Description | 240 | |-----------------|---------------------------------------------| 241 | | murphy:in | Fired when an element enters the viewport | 242 | | murphy:out | Fired when an element leaves the viewport | 243 | | murphy:finish | Fired when an animation completes | 244 | | murphy:cancel | Fired when an animation is cancelled | 245 | | murphy:reset | Fired when an element is reset | 246 | | murphy:cleanup | Fired when observers are cleaned up | 247 | 248 | **Example:** 249 | 250 | ```javascript copy 251 | document.addEventListener('murphy:in', ({ detail }) => { 252 | console.log('Element entered viewport:', detail.element); 253 | }); 254 | document.addEventListener('murphy:finish', ({ detail }) => { 255 | console.log('Animation finished:', detail.element); 256 | }); 257 | ``` -------------------------------------------------------------------------------- /pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Murphy.js' 3 | --- 4 | 5 | #

murphy.js

6 | 7 | A JavaScript vanilla library to scroll based reveal animations. 8 | The murphy.js is a lightweight JavaScript animation library with a simple implementation way. All this works by joining of data-attributes, Web animate API and Intersection Observer API. Now with a built-in event system for advanced control! 9 | 10 | ## Why use 11 | 12 | - ⚡️ Lightweight library (only 3.7KB) 13 | - 🍎 Easy and fast implementation 14 | - 🎮 Total control of IntersectionObserver parameters 15 | - 🎨 Full customization of time, duration, ease, delay and distance of each element individually 16 | - 🎁 Some animations implemented by default 17 | - 🏝 Plug and play solution to landing pages and simple projects 18 | - ❎ Native fallback to not supported browsers 19 | - 🛎️ Built-in event system for animation lifecycle (in, out, finish, cancel, reset, cleanup) 20 | 21 | ## Quick Start 22 | 23 | ### Installation 24 | 25 | ```bash copy copy 26 | npm install murphyjs 27 | ``` 28 | 29 | Or via CDN: 30 | ```html copy copy 31 | 32 | 38 | ``` 39 | 40 | ### Basic Usage 41 | 42 | Just do three steps: 43 | 44 | 1. Tag your HTML with `data-murphy`: 45 | ```html copy copy 46 |
Any content here
47 | ``` 48 | 49 | 2. (Optional) Reset your CSS to prevent initial visibility: 50 | ```css copy copy 51 | *[data-murphy] { 52 | opacity: 0; 53 | } 54 | ``` 55 | 56 | 3. Start murphy.js: 57 | ``` -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/src/.DS_Store -------------------------------------------------------------------------------- /src/core/config.js: -------------------------------------------------------------------------------- 1 | import BezierEasing from 'bezier-easing'; 2 | 3 | const config = { 4 | // Basic animations 5 | LEFT_TO_RIGHT: "left-to-right", 6 | RIGHT_TO_LEFT: "right-to-left", 7 | TOP_TO_BOTTOM: "top-to-bottom", 8 | BOTTOM_TO_TOP: "bottom-to-top", 9 | 10 | // Mobile detection 11 | MOBILE_BREAKPOINT: 768, // Standard mobile breakpoint 12 | MOBILE_REGEX: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i, 13 | 14 | // Flip animations 15 | FLIP_LEFT: "flip-left", 16 | FLIP_RIGHT: "flip-right", 17 | FLIP_UP: "flip-up", 18 | FLIP_DOWN: "flip-down", 19 | 20 | // Zoom animations 21 | ZOOM_IN: "zoom-in", 22 | ZOOM_OUT: "zoom-out", 23 | 24 | // Fade animations 25 | FADE: "fade", 26 | FADE_UP: "fade-up", 27 | FADE_DOWN: "fade-down", 28 | FADE_LEFT: "fade-left", 29 | FADE_RIGHT: "fade-right", 30 | 31 | // Rotate animations 32 | ROTATE_LEFT: "rotate-left", 33 | ROTATE_RIGHT: "rotate-right", 34 | 35 | // Scale animations 36 | SCALE_UP: "scale-up", 37 | SCALE_DOWN: "scale-down", 38 | 39 | // Slide animations 40 | SLIDE_UP: "slide-up", 41 | SLIDE_DOWN: "slide-down", 42 | SLIDE_LEFT: "slide-left", 43 | SLIDE_RIGHT: "slide-right", 44 | 45 | // Bounce animations 46 | BOUNCE_IN: "bounce-in", 47 | BOUNCE_OUT: "bounce-out", 48 | 49 | MURPHY_SELECTOR: "[data-murphy]", 50 | APPEARANCE_DISTANCE_DEFAULT: 100, 51 | ELEMENT_DISTANCE_DEFAULT: 50, 52 | EASE_DEFAULT: "cubic-bezier(0.25, 0.1, 0.25, 1)", 53 | ANIMATION_DELAY_DEFAULT: 0, 54 | THRESHOLD_DEFAULT: 0.1, 55 | ANIMATION_DURATION_DEFAULT: 300, 56 | 57 | // Viewport position aliases 58 | VIEWPORT_POSITIONS: { 59 | TOP: '0px 0px 0px 0px', // Triggers at top of viewport 60 | MIDDLE: '0px 0px -50% 0px', // Triggers at middle of viewport 61 | BOTTOM: '0px 0px 0px 0px', // Triggers at bottom of viewport 62 | QUARTER: '0px 0px -25% 0px', // Triggers at 25% from bottom 63 | THREE_QUARTERS: '0px 0px -75% 0px' // Triggers at 75% from bottom 64 | }, 65 | 66 | // Animation configurations 67 | ANIMATION_CONFIGS: { 68 | // Flip animations 69 | 'flip-left': { 70 | transform: 'rotateY(-90deg)', 71 | transformOrigin: 'left center', 72 | opacity: 0 73 | }, 74 | 'flip-right': { 75 | transform: 'rotateY(90deg)', 76 | transformOrigin: 'right center', 77 | opacity: 0 78 | }, 79 | 'flip-up': { 80 | transform: 'rotateX(-90deg)', 81 | transformOrigin: 'center top', 82 | opacity: 0 83 | }, 84 | 'flip-down': { 85 | transform: 'rotateX(90deg)', 86 | transformOrigin: 'center bottom', 87 | opacity: 0 88 | }, 89 | 90 | // Zoom animations 91 | 'zoom-in': { 92 | transform: 'scale(0.5)', 93 | opacity: 0 94 | }, 95 | 'zoom-out': { 96 | transform: 'scale(1.5)', 97 | opacity: 0 98 | }, 99 | 100 | // Fade animations 101 | 'fade': { 102 | opacity: 0 103 | }, 104 | 'fade-up': { 105 | transform: 'translateY(20px)', 106 | opacity: 0 107 | }, 108 | 'fade-down': { 109 | transform: 'translateY(-20px)', 110 | opacity: 0 111 | }, 112 | 'fade-left': { 113 | transform: 'translateX(20px)', 114 | opacity: 0 115 | }, 116 | 'fade-right': { 117 | transform: 'translateX(-20px)', 118 | opacity: 0 119 | }, 120 | 121 | // Rotate animations 122 | 'rotate-left': { 123 | transform: 'rotate(-180deg)', 124 | opacity: 0 125 | }, 126 | 'rotate-right': { 127 | transform: 'rotate(180deg)', 128 | opacity: 0 129 | }, 130 | 131 | // Scale animations 132 | 'scale-up': { 133 | transform: 'scale(0.5)', 134 | opacity: 0 135 | }, 136 | 'scale-down': { 137 | transform: 'scale(1.5)', 138 | opacity: 0 139 | }, 140 | 141 | // Slide animations 142 | 'slide-up': { 143 | transform: 'translateY(100%)', 144 | opacity: 0 145 | }, 146 | 'slide-down': { 147 | transform: 'translateY(-100%)', 148 | opacity: 0 149 | }, 150 | 'slide-left': { 151 | transform: 'translateX(100%)', 152 | opacity: 0 153 | }, 154 | 'slide-right': { 155 | transform: 'translateX(-100%)', 156 | opacity: 0 157 | }, 158 | 159 | // Bounce animations 160 | 'bounce-in': { 161 | transform: 'scale(0.3) translateY(100px)', 162 | opacity: 0 163 | }, 164 | 'bounce-out': { 165 | transform: 'scale(1.2) translateY(-50px)', 166 | opacity: 0 167 | }, 168 | 'elastic': { 169 | transform: 'scale(0.5) translateY(80px)', 170 | opacity: 0 171 | }, 172 | 'spring': { 173 | transform: 'scale(0.8) translateY(60px)', 174 | opacity: 0 175 | }, 176 | 'smooth': [0.4, 0, 0.2, 1], 177 | 'sharp': [0.4, 0, 0.6, 1], 178 | 'swift': [0.4, 0, 0.2, 1], 179 | 'material-standard': [0.4, 0, 0.2, 1], 180 | 'material-decelerate': [0, 0, 0.2, 1], 181 | 'material-accelerate': [0.4, 0, 1, 1] 182 | }, 183 | 184 | // Bezier easing functions for JavaScript animations 185 | BEZIER_EASINGS: { 186 | // Standard CSS timing functions 187 | 'ease': [0.25, 0.1, 0.25, 1], 188 | 'linear': [0, 0, 1, 1], 189 | 'ease-in': [0.42, 0, 1, 1], 190 | 'ease-out': [0, 0, 0.58, 1], 191 | 'ease-in-out': [0.42, 0, 0.58, 1], 192 | 193 | // Material Design easings 194 | 'material-standard': [0.4, 0.0, 0.2, 1], 195 | 'material-decelerate': [0.0, 0.0, 0.2, 1], 196 | 'material-accelerate': [0.4, 0.0, 1, 1], 197 | 198 | // Custom easings 199 | 'bounce': [0.68, -0.6, 0.32, 1.6], 200 | 'elastic': [0.68, -0.6, 0.32, 1.6], 201 | 'smooth': [0.4, 0, 0.2, 1], 202 | 'sharp': [0.4, 0, 0.6, 1], 203 | 'swift': [0.4, 0, 0.2, 1], 204 | 'spring': [0.68, -0.6, 0.32, 1.6] 205 | } 206 | }; 207 | 208 | export default config; 209 | 210 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | import config from "./config.js"; 2 | import BezierEasing from 'bezier-easing'; 3 | 4 | const { 5 | LEFT_TO_RIGHT, 6 | RIGHT_TO_LEFT, 7 | TOP_TO_BOTTOM, 8 | BOTTOM_TO_TOP, 9 | MURPHY_SELECTOR, 10 | APPEARANCE_DISTANCE_DEFAULT, 11 | ELEMENT_DISTANCE_DEFAULT, 12 | ANIMATION_DELAY_DEFAULT, 13 | THRESHOLD_DEFAULT, 14 | ANIMATION_DURATION_DEFAULT, 15 | BEZIER_EASINGS 16 | } = config; 17 | 18 | // Event system 19 | const dispatchEvent = (element, eventName, detail = {}) => { 20 | const event = new CustomEvent(`murphy:${eventName}`, { 21 | detail: { element, ...detail } 22 | }); 23 | document.dispatchEvent(event); 24 | }; 25 | 26 | const debounce = (fn, delay) => { 27 | let timeoutId; 28 | return (...args) => { 29 | clearTimeout(timeoutId); 30 | timeoutId = setTimeout(() => fn(...args), delay); 31 | }; 32 | }; 33 | 34 | const play = (group) => { 35 | if (!murphyWillWork()) return cancel(); 36 | let selector = MURPHY_SELECTOR; 37 | if (group) selector += `[data-murphy-group="${group}"]`; 38 | const elements = document.querySelectorAll(selector); 39 | return elements.forEach(element => { 40 | startAnimation(element); 41 | }); 42 | }; 43 | 44 | const cancel = () => { 45 | const elements = document.querySelectorAll(MURPHY_SELECTOR); 46 | elements.forEach(element => { 47 | element.style.opacity = '1'; 48 | element.style.transform = 'translate(0)'; 49 | element.classList.add('murphy-animated'); 50 | dispatchEvent(element, 'cancel'); 51 | }); 52 | }; 53 | 54 | const reset = (group) => { 55 | let selector = MURPHY_SELECTOR; 56 | if (group) selector += `[data-murphy-group="${group}"]`; 57 | const elements = document.querySelectorAll(selector); 58 | elements.forEach(element => { 59 | // Force element to final state before animating back 60 | element.style.opacity = '1'; 61 | element.style.transform = 'none'; 62 | // Force reflow 63 | void element.offsetWidth; 64 | 65 | // Get the current animation configuration 66 | const animationType = element.dataset.murphy || BOTTOM_TO_TOP; 67 | const elementDistance = element.dataset.murphyElementDistance || ELEMENT_DISTANCE_DEFAULT; 68 | const duration = parseInt(element.dataset.murphyAnimationDuration) || ANIMATION_DURATION_DEFAULT; 69 | const ease = element.dataset.murphyEase || config.EASE_DEFAULT; 70 | 71 | // Create reverse animation 72 | const cubicBezierValue = getEasingValue(ease); 73 | const initialTransform = getInitialTransform(animationType, elementDistance); 74 | 75 | const animation = element.animate( 76 | [ 77 | { 78 | transform: 'none', 79 | opacity: 1 80 | }, 81 | { 82 | transform: initialTransform, 83 | opacity: 0 84 | } 85 | ], 86 | { 87 | duration, 88 | easing: cubicBezierValue, 89 | fill: 'forwards' 90 | } 91 | ); 92 | 93 | // Store animation reference 94 | element._animation = animation; 95 | 96 | // Handle animation completion 97 | animation.onfinish = () => { 98 | // Remove all murphy-related classes 99 | element.classList.remove('murphy-animated'); 100 | element.classList.remove('murphy-in'); 101 | element.classList.remove('murphy-out'); 102 | 103 | // Reset animation reference 104 | delete element._animation; 105 | 106 | // Re-observe the element 107 | if (element._observer) { 108 | element._observer.disconnect(); 109 | delete element._observer; 110 | } 111 | 112 | dispatchEvent(element, 'reset'); 113 | }; 114 | }); 115 | }; 116 | 117 | // Cleanup function to disconnect observers 118 | const cleanup = () => { 119 | const elements = document.querySelectorAll(MURPHY_SELECTOR); 120 | elements.forEach(element => { 121 | if (element._observer) { 122 | element._observer.disconnect(); 123 | delete element._observer; 124 | } 125 | dispatchEvent(element, 'cleanup'); 126 | }); 127 | }; 128 | 129 | const isMobile = () => { 130 | return ( 131 | window.innerWidth <= config.MOBILE_BREAKPOINT || 132 | config.MOBILE_REGEX.test(navigator.userAgent) 133 | ); 134 | }; 135 | 136 | const startAnimation = element => { 137 | // Check if animations should be disabled on mobile 138 | if (element.dataset.murphyDisableMobile === 'true' && isMobile()) { 139 | // Set element to final state without animation 140 | element.style.opacity = '1'; 141 | element.style.transform = 'none'; 142 | return; 143 | } 144 | 145 | const animationType = element.dataset.murphy || BOTTOM_TO_TOP; 146 | const appearanceDistance = element.dataset.murphyAppearanceDistance || APPEARANCE_DISTANCE_DEFAULT; 147 | const elementDistance = element.dataset.murphyElementDistance || ELEMENT_DISTANCE_DEFAULT; 148 | const easeName = element.dataset.murphyEase || 'ease'; 149 | const delay = parseInt(element.dataset.murphyAnimationDelay) || ANIMATION_DELAY_DEFAULT; 150 | const elementThreshold = parseFloat(element.dataset.murphyElementThreshold) || THRESHOLD_DEFAULT; 151 | const animationDuration = parseInt(element.dataset.murphyAnimationDuration) || ANIMATION_DURATION_DEFAULT; 152 | 153 | // Set initial state 154 | element.style.opacity = '0'; 155 | element.style.transform = getInitialTransform(animationType, elementDistance); 156 | 157 | // Handle viewport position aliases 158 | let rootMargin = element.dataset.murphyRootMargin; 159 | if (rootMargin) { 160 | // Check if the value is an alias 161 | const alias = rootMargin.toUpperCase(); 162 | if (config.VIEWPORT_POSITIONS[alias]) { 163 | rootMargin = config.VIEWPORT_POSITIONS[alias]; 164 | } 165 | } else { 166 | rootMargin = `0px 0px ${appearanceDistance * -1}px 0px`; 167 | } 168 | 169 | const observerOptions = { 170 | threshold: elementThreshold, 171 | rootMargin 172 | }; 173 | 174 | const elementOptions = { 175 | element, 176 | animationType, 177 | animationDuration, 178 | elementDistance, 179 | ease: easeName, 180 | delay, 181 | elementThreshold 182 | }; 183 | 184 | generateIntersectionObserver({ elementOptions, observerOptions }); 185 | }; 186 | 187 | const getInitialTransform = (animationType, distance) => { 188 | const transforms = { 189 | [BOTTOM_TO_TOP]: `translateY(${distance}px)`, 190 | [TOP_TO_BOTTOM]: `translateY(-${distance}px)`, 191 | [LEFT_TO_RIGHT]: `translateX(-${distance}px)`, 192 | [RIGHT_TO_LEFT]: `translateX(${distance}px)` 193 | }; 194 | return transforms[animationType] || transforms[BOTTOM_TO_TOP]; 195 | }; 196 | 197 | const generateIntersectionObserver = ({ elementOptions, observerOptions }) => { 198 | try { 199 | const element = elementOptions.element; 200 | const animationType = elementOptions.animationType; 201 | const shouldMirror = element.dataset.murphyMirror === 'true'; 202 | 203 | const observer = new IntersectionObserver( 204 | debounce(entries => { 205 | entries.forEach(entry => { 206 | const { intersectionRatio } = entry; 207 | const elementThreshold = Number(elementOptions.elementThreshold) || 0; 208 | 209 | // Add a small buffer to prevent rapid toggling 210 | const buffer = 0.05; 211 | const isFullyInView = intersectionRatio >= (elementThreshold - buffer); 212 | const isFullyOutOfView = intersectionRatio <= buffer; 213 | const isInBufferZone = intersectionRatio > buffer && intersectionRatio < (elementThreshold - buffer); 214 | 215 | // Only trigger animations when fully in or out of view, ignoring buffer zone 216 | if (isFullyInView && !isInBufferZone) { 217 | const animate = generateAnimate(element, { 218 | delay: elementOptions.delay, 219 | duration: elementOptions.animationDuration, 220 | easing: elementOptions.ease, 221 | distance: elementOptions.elementDistance, 222 | direction: animationType 223 | }); 224 | animate(); 225 | if (!shouldMirror) { 226 | observer.unobserve(entry.target); 227 | } 228 | dispatchEvent(element, 'in', { intersectionRatio }); 229 | } else if (shouldMirror && isFullyOutOfView && !isInBufferZone) { 230 | // Play animation in reverse only when fully out of view 231 | const reverseAnimate = generateAnimate(element, { 232 | delay: 0, 233 | duration: elementOptions.animationDuration, 234 | easing: elementOptions.ease, 235 | distance: elementOptions.elementDistance, 236 | direction: animationType, 237 | reverse: true 238 | }); 239 | reverseAnimate(); 240 | dispatchEvent(element, 'out', { intersectionRatio }); 241 | } 242 | // Ignore intermediate states in buffer zone to prevent blinking 243 | }); 244 | }, 100), 245 | observerOptions 246 | ); 247 | 248 | // Store observer reference for cleanup 249 | element._observer = observer; 250 | observer.observe(element); 251 | } catch (error) { 252 | // Fallback to immediate animation 253 | const animate = generateAnimate(element, { 254 | delay: elementOptions.delay, 255 | duration: elementOptions.animationDuration, 256 | easing: elementOptions.ease, 257 | distance: elementOptions.elementDistance, 258 | direction: animationType 259 | }); 260 | animate(); 261 | dispatchEvent(element, 'in', { error: 'IntersectionObserver not supported' }); 262 | } 263 | }; 264 | 265 | function generateAnimate(element, options) { 266 | const { 267 | delay = config.ANIMATION_DELAY_DEFAULT, 268 | duration = config.ANIMATION_DURATION_DEFAULT, 269 | easing = config.EASE_DEFAULT, 270 | distance = config.ELEMENT_DISTANCE_DEFAULT, 271 | direction = config.LEFT_TO_RIGHT, 272 | reverse = false 273 | } = options; 274 | 275 | // Validate animation parameters 276 | const validDuration = Math.max(0, Number(duration) || config.ANIMATION_DURATION_DEFAULT); 277 | const validDelay = Math.max(0, Number(delay) || config.ANIMATION_DELAY_DEFAULT); 278 | const validDistance = Math.max(0, Number(distance) || config.ELEMENT_DISTANCE_DEFAULT); 279 | 280 | return () => { 281 | setTimeout(() => { 282 | const cubicBezierValue = getEasingValue(easing); 283 | 284 | // Get animation configuration based on direction 285 | const animationConfig = config.ANIMATION_CONFIGS[direction] || { 286 | transform: getTransformValue(direction, validDistance), 287 | opacity: 0 288 | }; 289 | 290 | // Create animation with reversed keyframes if needed 291 | const animation = element.animate( 292 | reverse ? [ 293 | { 294 | transform: 'none', 295 | opacity: 1 296 | }, 297 | animationConfig 298 | ] : [ 299 | animationConfig, 300 | { 301 | transform: 'none', 302 | opacity: 1 303 | } 304 | ], 305 | { 306 | duration: validDuration, 307 | easing: cubicBezierValue, 308 | fill: 'forwards' 309 | } 310 | ); 311 | 312 | // Store animation reference for reset 313 | element._animation = animation; 314 | 315 | animation.onfinish = () => { 316 | if (!reverse) { 317 | element.classList.add('murphy-animated'); 318 | } else { 319 | element.classList.remove('murphy-animated'); 320 | } 321 | dispatchEvent(element, 'finish', { reverse }); 322 | }; 323 | }, validDelay); 324 | }; 325 | } 326 | 327 | function getTransformValue(direction, distance) { 328 | switch (direction) { 329 | case config.LEFT_TO_RIGHT: 330 | return `translateX(-${distance}px)`; 331 | case config.RIGHT_TO_LEFT: 332 | return `translateX(${distance}px)`; 333 | case config.TOP_TO_BOTTOM: 334 | return `translateY(-${distance}px)`; 335 | case config.BOTTOM_TO_TOP: 336 | return `translateY(${distance}px)`; 337 | default: 338 | return 'none'; 339 | } 340 | } 341 | 342 | function getEasingValue(easing) { 343 | if (typeof easing === 'string') { 344 | // Check if it's a cubic-bezier value 345 | if (easing.startsWith('cubic-bezier')) { 346 | return easing; 347 | } 348 | // Check if it's a predefined easing 349 | const bezierPoints = config.BEZIER_EASINGS[easing]; 350 | if (bezierPoints && Array.isArray(bezierPoints) && bezierPoints.length === 4) { 351 | return `cubic-bezier(${bezierPoints.join(', ')})`; 352 | } 353 | } 354 | return config.EASE_DEFAULT; 355 | } 356 | 357 | const observerIsSupported = () => { 358 | return !!( 359 | window.IntersectionObserver && 360 | "IntersectionObserver" in window && 361 | "IntersectionObserverEntry" in window && 362 | "intersectionRatio" in window.IntersectionObserverEntry.prototype 363 | ); 364 | }; 365 | 366 | const animationIsSupported = () => { 367 | return !!window.Animation; 368 | }; 369 | 370 | const murphyWillWork = () => { 371 | return animationIsSupported() && observerIsSupported(); 372 | }; 373 | 374 | class Murphy { 375 | constructor() { 376 | if (!murphyWillWork()) { 377 | console.warn('MurphyJS: Your browser does not support required features'); 378 | } 379 | } 380 | 381 | animate(selector, options) { 382 | const elements = document.querySelectorAll(selector); 383 | const { 384 | opacity = [0, 1], 385 | x = [0, 0], 386 | y = [0, 0], 387 | duration = 1000, 388 | delay = 0, 389 | ease = 'ease' 390 | } = options; 391 | 392 | elements.forEach(element => { 393 | // Set initial state 394 | element.style.opacity = opacity[0]; 395 | element.style.transform = `translate(${x[0]}px, ${y[0]}px)`; 396 | 397 | // Add delay using setTimeout 398 | setTimeout(() => { 399 | const bezierEasing = BEZIER_EASINGS[ease] || BezierEasing(0.4, 0.0, 0.2, 1); 400 | const startTime = performance.now(); 401 | 402 | const animate = (currentTime) => { 403 | const elapsed = currentTime - startTime; 404 | const progress = Math.min(elapsed / duration, 1); 405 | const easedProgress = bezierEasing(progress); 406 | 407 | // Interpolate values 408 | const currentOpacity = opacity[0] + (opacity[1] - opacity[0]) * easedProgress; 409 | const currentX = x[0] + (x[1] - x[0]) * easedProgress; 410 | const currentY = y[0] + (y[1] - y[0]) * easedProgress; 411 | 412 | element.style.opacity = currentOpacity; 413 | element.style.transform = `translate(${currentX}px, ${currentY}px)`; 414 | 415 | if (progress < 1) { 416 | requestAnimationFrame(animate); 417 | } else { 418 | element.style.opacity = opacity[1]; 419 | element.style.transform = `translate(${x[1]}px, ${y[1]}px)`; 420 | dispatchEvent(element, 'finish'); 421 | } 422 | }; 423 | 424 | requestAnimationFrame(animate); 425 | }, delay); 426 | }); 427 | } 428 | } 429 | 430 | // Only attach to window if we're in a browser environment 431 | if (typeof window !== 'undefined') { 432 | window.murphy = { play, cancel, reset, cleanup }; 433 | window.Murphy = Murphy; 434 | } 435 | 436 | export { play, cancel, reset, cleanup, Murphy }; 437 | export default { play, cancel, reset, cleanup, Murphy }; -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cesarolvr/murphyjs/e60256f01938cc0e686a56cf9ab69beb1c91a736/static/favicon.ico -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | /* Google Fonts */ 2 | @import url("https://fonts.googleapis.com/css2?family=Jersey+15&display=swap"); 3 | 4 | html, 5 | html[class~="dark"], 6 | .dark { 7 | color-scheme: dark !important; 8 | } 9 | 10 | /* Global Outline */ 11 | *:focus { 12 | outline: 1px solid #ff9696 !important; 13 | outline-offset: 1px !important; 14 | box-shadow: none !important; 15 | } 16 | 17 | *:focus:not(:focus-visible) { 18 | outline: none !important; 19 | box-shadow: none !important; 20 | } 21 | 22 | *:focus-visible { 23 | outline: 1px solid #ff9696 !important; 24 | outline-offset: 1px !important; 25 | box-shadow: none !important; 26 | } 27 | 28 | /* Remove default focus styles */ 29 | * { 30 | -webkit-tap-highlight-color: transparent !important; 31 | } 32 | 33 | /* Hide Theme Toggle */ 34 | button[aria-label="Toggle Dark Mode"], 35 | button[aria-label="Toggle Light Mode"], 36 | button[aria-label="Change theme"], 37 | .nextra-nav-container button[aria-label="Change theme"], 38 | .nextra-nav-container button[aria-label="Toggle Dark Mode"], 39 | .nextra-nav-container button[aria-label="Toggle Light Mode"], 40 | button[id^="headlessui-listbox-button-"], 41 | [role="listbox"], 42 | [role="option"] { 43 | display: none !important; 44 | } 45 | 46 | /* Hide Theme Menu */ 47 | .nextra-nav-container [role="menu"], 48 | .nextra-nav-container [role="menuitem"], 49 | .nextra-nav-container [role="listbox"], 50 | .nextra-nav-container [role="option"] { 51 | display: none !important; 52 | } 53 | 54 | /* Header Title */ 55 | :root { 56 | --header-font: "Jersey 15", sans-serif; 57 | --murphy-ease: ease; 58 | } 59 | 60 | /* Overrides */ 61 | .nx-text-primary-600 { 62 | color: #ff9696 !important; 63 | } 64 | 65 | input:focus-visible { 66 | /* border-color: #ff9696 !important; */ 67 | box-shadow: 0 0 0 2px #ff9696 !important; 68 | } 69 | 70 | :is(html[class~="dark"] .dark\:nx-bg-primary-400\/10), 71 | :is(html[class~="dark"] .dark\:nx-bg-primary-300\/10) { 72 | background-color: #ffffff17 !important; 73 | } 74 | 75 | :is(html[class~="dark"] .dark\:nx-text-primary-600), 76 | .nextra-content, 77 | :is(html[class~="dark"] .dark\:nx-text-slate-100), 78 | .nx-text-slate-900 { 79 | color: #ffffff !important; 80 | } 81 | 82 | .footer { 83 | max-width: 90rem; 84 | margin: 0 auto; 85 | padding: 0 max(env(safe-area-inset-right), 1.5rem) 40px; 86 | font-family: var(--header-font) !important; 87 | font-size: 2rem !important; 88 | letter-spacing: 1px; 89 | background: linear-gradient(45deg, #f97b7b, #ffbbbb); 90 | text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1); 91 | -webkit-background-clip: text; 92 | -webkit-text-fill-color: transparent; 93 | } 94 | 95 | /* Code Blocks and Syntax Highlighting */ 96 | .nx-bg-neutral-100 { 97 | background-color: #2d1f1f !important; 98 | } 99 | 100 | .dark .nx-bg-neutral-800 { 101 | background-color: #2d1f1f !important; 102 | } 103 | 104 | /* Bash/Shell specific colors */ 105 | .language-bash .nx-text-primary-600, 106 | .language-shell .nx-text-primary-600 { 107 | color: #ff9696 !important; 108 | } 109 | 110 | .language-bash .nx-text-yellow-600, 111 | .language-shell .nx-text-yellow-600 { 112 | color: #ff6b6b !important; 113 | } 114 | 115 | .language-bash .nx-text-green-600, 116 | .language-shell .nx-text-green-600 { 117 | color: #f97b7b !important; 118 | } 119 | 120 | .language-bash .nx-text-blue-600, 121 | .language-shell .nx-text-blue-600 { 122 | color: #ffbbbb !important; 123 | } 124 | 125 | /* Code block border and shadow */ 126 | .nx-border-neutral-200 { 127 | border-color: #3d2f2f !important; 128 | } 129 | 130 | .dark .nx-border-neutral-800 { 131 | border-color: #3d2f2f !important; 132 | } 133 | 134 | .nx-shadow-sm { 135 | box-shadow: 0 2px 4px rgba(255, 150, 150, 0.1) !important; 136 | } 137 | 138 | /* Force dark background */ 139 | body { 140 | background-color: #111 !important; 141 | color: #fff !important; 142 | } 143 | 144 | /* Demo container dark mode */ 145 | .demo-container { 146 | margin: 2rem 0; 147 | padding: 2rem; 148 | border: 1px solid #ffffff17 !important; 149 | border-radius: 12px; 150 | background: #ffffff17 !important; 151 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); 152 | transition: all 0.3s ease; 153 | } 154 | 155 | .demo-container:hover { 156 | box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1); 157 | transform: translateY(-2px); 158 | } 159 | 160 | /* Initial state for demo elements */ 161 | /* [data-murphy]:not(.murphy-animated) { 162 | opacity: 0; 163 | } */ 164 | 165 | /* Demo box styles */ 166 | .demo-box { 167 | padding: 1.5rem; 168 | margin: 1rem 0; 169 | background: #1a1a1a !important; 170 | color: #fff !important; 171 | border-radius: 8px; 172 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); 173 | text-align: center; 174 | display: flex; 175 | align-items: center; 176 | justify-content: center; 177 | gap: 0.5rem; 178 | } 179 | 180 | .demo-box strong { 181 | color: #ff9696; 182 | font-weight: 600; 183 | } 184 | 185 | .demo-box:hover { 186 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 187 | transform: translateY(-1px); 188 | } 189 | 190 | /* Animation direction indicators */ 191 | .demo-box[data-murphy="bottom-to-top"]::after { 192 | content: "👆"; 193 | font-size: 1.2rem; 194 | opacity: 0.7; 195 | } 196 | 197 | .demo-box[data-murphy="top-to-bottom"]::after { 198 | content: "👇"; 199 | font-size: 1.2rem; 200 | opacity: 0.7; 201 | } 202 | 203 | .demo-box[data-murphy="left-to-right"]::after { 204 | content: "👉"; 205 | font-size: 1.2rem; 206 | opacity: 0.7; 207 | } 208 | 209 | .demo-box[data-murphy="right-to-left"]::after { 210 | content: "👈"; 211 | font-size: 1.2rem; 212 | opacity: 0.7; 213 | } 214 | 215 | .logo-demo { 216 | display: flex; 217 | justify-content: center; 218 | align-items: center; 219 | gap: 0.5rem; 220 | /* font-size: 5rem; */ 221 | font-weight: bold; 222 | color: #ff9696; 223 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 224 | } 225 | 226 | .demo-letter { 227 | opacity: 0; 228 | transition: all 0.3s ease; 229 | font-family: var(--header-font); 230 | font-size: 5rem; 231 | letter-spacing: 1px; 232 | background: linear-gradient(45deg, #f97b7b, #ffbbbb); 233 | -webkit-background-clip: text; 234 | -webkit-text-fill-color: transparent; 235 | text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1); 236 | 237 | @media (max-width: 768px) { 238 | font-size: 2rem; 239 | } 240 | } 241 | 242 | .demo-controls { 243 | display: flex; 244 | justify-content: end; 245 | gap: 1rem; 246 | margin-top: 1.5rem; 247 | } 248 | 249 | .demo-controls-fixed { 250 | position: fixed; 251 | bottom: 2rem; 252 | left: 50%; 253 | transform: translateX(-50%); 254 | z-index: 1000; 255 | background: rgba(26, 26, 26, 0.9); 256 | padding: 1rem 1rem; 257 | border-radius: 14px; 258 | backdrop-filter: blur(8px); 259 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 260 | border: 1px solid rgba(255, 255, 255, 0.1); 261 | display: flex; 262 | gap: 1rem; 263 | justify-content: center; 264 | min-width: 200px; 265 | } 266 | 267 | .demo-button { 268 | padding: 0.75rem 1.5rem; 269 | border: none; 270 | border-radius: 12px; 271 | color: white; 272 | font-size: 1rem; 273 | font-weight: 500; 274 | cursor: pointer; 275 | transition: all 0.3s ease; 276 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 277 | } 278 | 279 | .demo-button { 280 | background: linear-gradient(45deg, #ff6d6d, #ff9595); 281 | font-family: var(--header-font); 282 | font-size: 1.4rem; 283 | letter-spacing: 1px; 284 | } 285 | 286 | .demo-button.reset-button { 287 | background: #1a1a1a; 288 | border: 1px solid #414141; 289 | } 290 | 291 | .demo-button.reset-button:hover { 292 | background: #2a2a2a; 293 | transform: translateY(-1px); 294 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 295 | } 296 | 297 | .demo-button.reset-button:active { 298 | background: #000; 299 | transform: translateY(0); 300 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 301 | } 302 | 303 | .demo-button:not(.reset-button):hover { 304 | background: linear-gradient(45deg, #ff5757, #ff8383); 305 | transform: translateY(-1px); 306 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 307 | } 308 | 309 | .demo-button:not(.reset-button):active { 310 | background: linear-gradient(45deg, #ff5757, #ff8383); 311 | transform: translateY(0); 312 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 313 | } 314 | 315 | /* Sobrescrever o estilo do título no header do Nextra */ 316 | .nextra-nav-container .nextra-nav-container-blur { 317 | background: rgba(255, 255, 255, 0.8) !important; 318 | backdrop-filter: blur(8px); 319 | } 320 | 321 | .nextra-nav-container 322 | .nextra-nav-container-blur 323 | .dark\:nextra-nav-container-blur { 324 | background: rgba(17, 17, 17, 0.8) !important; 325 | } 326 | 327 | .nextra-nav-container a[href="/"], 328 | .nextra-footer a[href="/"] { 329 | font-family: var(--header-font) !important; 330 | font-size: 2rem !important; 331 | letter-spacing: 1px; 332 | background: linear-gradient(45deg, #f97b7b, #ffbbbb); 333 | -webkit-background-clip: text; 334 | -webkit-text-fill-color: transparent; 335 | text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1); 336 | transition: all 0.3s ease; 337 | text-transform: lowercase; 338 | } 339 | 340 | .nextra-nav-container a[href="/"]:hover, 341 | .nextra-footer a[href="/"]:hover { 342 | transform: translateY(-2px); 343 | text-shadow: 4px 4px 0px rgba(0, 0, 0, 0.15); 344 | } 345 | 346 | .note { 347 | background: rgba(255, 150, 150, 0.1); 348 | border-left: 4px solid #ff9696; 349 | padding: 1rem; 350 | margin: 1rem 0; 351 | border-radius: 0 8px 8px 0; 352 | font-size: 0.95rem; 353 | line-height: 1.5; 354 | color: #fff; 355 | } 356 | 357 | .note strong { 358 | color: #ff9696; 359 | font-weight: 600; 360 | } 361 | 362 | /* Custom text selection */ 363 | ::selection { 364 | background: #ff9696; 365 | color: #111111; 366 | text-shadow: none; 367 | } 368 | 369 | /* Add hover effects */ 370 | a:hover { 371 | color: #ff9696; 372 | } 373 | 374 | .pink { 375 | font-family: var(--header-font) !important; 376 | font-size: 4.5rem !important; 377 | letter-spacing: 1px; 378 | background: linear-gradient(45deg, #f97b7b, #ffbbbb); 379 | text-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1); 380 | -webkit-background-clip: text; 381 | -webkit-text-fill-color: transparent; 382 | } 383 | 384 | /* Add focus styles */ 385 | input:focus { 386 | outline: 2px solid #ff9696; 387 | outline-offset: 2px; 388 | } 389 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx}', 5 | './src/components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /theme.config.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DocsThemeConfig } from 'nextra-theme-docs' 3 | 4 | const config: DocsThemeConfig = { 5 | logo: murphy.js, 6 | project: { 7 | link: 'https://github.com/cesarolvr/murphyjs', 8 | }, 9 | docsRepositoryBase: 'https://github.com/cesarolvr/murphyjs', 10 | footer: { 11 | component:
murphy.js
12 | }, 13 | useNextSeoProps() { 14 | return { 15 | titleTemplate: '%s' 16 | } 17 | }, 18 | head: ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | ), 26 | nextThemes: { 27 | defaultTheme: 'dark', 28 | }, 29 | } 30 | 31 | export default config -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.mdx"], 23 | "exclude": ["node_modules"] 24 | } -------------------------------------------------------------------------------- /types/murphy.d.ts: -------------------------------------------------------------------------------- 1 | interface Murphy { 2 | play: (group?: string) => void; 3 | pause: () => void; 4 | stop: () => void; 5 | reset: (group?: string) => void; 6 | } 7 | 8 | declare global { 9 | interface Window { 10 | murphy: Murphy; 11 | } 12 | } 13 | 14 | export {}; -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "yarn build:docs", 3 | "outputDirectory": ".next", 4 | "framework": "nextjs" 5 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/core/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'index.js', 9 | library: { 10 | name: 'murphy', 11 | type: 'umd', 12 | export: 'default' 13 | }, 14 | globalObject: 'this', 15 | clean: true, 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | babelrc: false, 26 | configFile: path.resolve(__dirname, 'babel.librc') 27 | } 28 | }, 29 | }, 30 | ], 31 | }, 32 | plugins: [ 33 | new CopyWebpackPlugin({ 34 | patterns: [ 35 | { from: 'logo.png', to: 'logo.png' } 36 | ], 37 | }), 38 | ], 39 | resolve: { 40 | extensions: ['.js'], 41 | }, 42 | mode: 'production', 43 | optimization: { 44 | minimize: true 45 | }, 46 | devServer: { 47 | static: { 48 | directory: path.join(__dirname, '/'), 49 | }, 50 | compress: true, 51 | port: 9000, 52 | hot: true, 53 | }, 54 | }; --------------------------------------------------------------------------------