react-manga-effects 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/components/IrisWipe/IrisWipe.d.ts +4 -0
- package/dist/components/IrisWipe/index.d.ts +1 -0
- package/dist/components/Placeholder/Placeholder.d.ts +6 -0
- package/dist/components/SpeedLines/SpeedLines.d.ts +4 -0
- package/dist/components/SpeedLines/hooks/useAnimationLoop.d.ts +1 -0
- package/dist/components/SpeedLines/index.d.ts +1 -0
- package/dist/components/SpeedLines/types.d.ts +13 -0
- package/dist/components/SpeedLines/utils/lineGenerator.d.ts +6 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +205 -0
- package/dist/style.css +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/types/index.d.ts +68 -0
- package/dist/utils/easing.d.ts +13 -0
- package/package.json +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 erutobusiness
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# 🎬 react-manga-effects
|
|
2
|
+
|
|
3
|
+
> Add dramatic anime/manga impact effects to your React applications with zero fuss.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
**react-manga-effects** provides lightweight, customizable high-impact visual effects common in anime and manga, such as circular iris wipes and dynamic focus lines. Built with TypeScript and optimizing for performance.
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
*(Placeholder: Effects demo appearing here)*
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **🌀 IrisWipe**: Classic anime-style circular transition to open/close scenes.
|
|
19
|
+
- **âš¡ SpeedLines**: Dynamic focus/concentration lines (motion lines) to emphasize action or shock.
|
|
20
|
+
- **📘 TypeScript Support**: Fully typed props and exports.
|
|
21
|
+
- **🎈 Lightweight**: Zero runtime dependencies (other than React).
|
|
22
|
+
- **🚀 SSR Compatible**: Works with Next.js, Remix, and Gatsby.
|
|
23
|
+
- **🎨 Highly Customizable**: Control colors, density, speed, easing, and positioning.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @erutobusiness/react-manga-effects
|
|
29
|
+
# or
|
|
30
|
+
yarn add @erutobusiness/react-manga-effects
|
|
31
|
+
# or
|
|
32
|
+
pnpm add @erutobusiness/react-manga-effects
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Import the components and use them in your React app:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import React, { useState } from 'react';
|
|
41
|
+
import { IrisWipe, SpeedLines } from '@erutobusiness/react-manga-effects';
|
|
42
|
+
|
|
43
|
+
const App = () => {
|
|
44
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
|
|
48
|
+
{/* Background Content */}
|
|
49
|
+
<img src="/my-manga-scene.jpg" alt="Scene" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
50
|
+
|
|
51
|
+
{/* Overlay Effects */}
|
|
52
|
+
<SpeedLines
|
|
53
|
+
animated
|
|
54
|
+
color="rgba(0,0,0,0.5)"
|
|
55
|
+
center={{ x: 50, y: 50 }}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<IrisWipe
|
|
59
|
+
isOpen={isOpen}
|
|
60
|
+
duration={1000}
|
|
61
|
+
center={{ x: 50, y: 50 }}
|
|
62
|
+
onComplete={() => console.log('Transition Complete')}
|
|
63
|
+
>
|
|
64
|
+
{/* Content to be revealed/hidden inside the wipe can go here if needed,
|
|
65
|
+
typically IrisWipe is used as an overlay. */}
|
|
66
|
+
</IrisWipe>
|
|
67
|
+
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
70
|
+
style={{ position: 'absolute', bottom: 20, left: 20, zIndex: 100 }}
|
|
71
|
+
>
|
|
72
|
+
Toggle Transition
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default App;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Components API
|
|
82
|
+
|
|
83
|
+
### IrisWipe
|
|
84
|
+
|
|
85
|
+
A circular masking transition effect.
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Default | Description |
|
|
88
|
+
|------|------|---------|-------------|
|
|
89
|
+
| `isOpen` | `boolean` | **Required** | `true` to reveal content (open iris), `false` to hide (close iris). |
|
|
90
|
+
| `duration` | `number` | `500` | Animation duration in milliseconds. |
|
|
91
|
+
| `center` | `{ x: number, y: number }` | `{ x: 50, y: 50 }` | Center point of the iris in percentage (0-100). |
|
|
92
|
+
| `easing` | `string` | `'easeInOut'` | CSS transition timing function (e.g., `'linear'`, `'ease-out'`, `'cubic-bezier(...)`'). |
|
|
93
|
+
| `onComplete` | `() => void` | `undefined` | Callback function fired when the transition finishes. |
|
|
94
|
+
| `className` | `string` | `''` | Additional CSS classes for the container. |
|
|
95
|
+
| `style` | `CSSProperties` | `{}` | Inline styles for the container. |
|
|
96
|
+
|
|
97
|
+
#### Example: Custom Center
|
|
98
|
+
```tsx
|
|
99
|
+
<IrisWipe
|
|
100
|
+
isOpen={show}
|
|
101
|
+
center={{ x: 80, y: 20 }} // Focus on top-right
|
|
102
|
+
duration={1200}
|
|
103
|
+
/>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### SpeedLines
|
|
107
|
+
|
|
108
|
+
Concentration lines typically used to show speed, shock, or intense focus.
|
|
109
|
+
|
|
110
|
+
| Prop | Type | Default | Description |
|
|
111
|
+
|------|------|---------|-------------|
|
|
112
|
+
| `lineCount` | `number` | `60` | Number of lines to draw. Higher values create a denser effect. |
|
|
113
|
+
| `color` | `string` | `'rgba(0, 0, 0, 0.6)'` | Color of the lines. Supports valid CSS colors. |
|
|
114
|
+
| `minLength` | `number` | `10` | Minimum length of lines as a percentage of the container size. |
|
|
115
|
+
| `maxLength` | `number` | `30` | Maximum length of lines as a percentage of the container size. |
|
|
116
|
+
| `innerRadius` | `number` | `0` | Radius of the empty safe zone in the center (percentage). |
|
|
117
|
+
| `center` | `{ x: number, y: number }` | `{ x: 50, y: 50 }` | The convergence point of the lines. |
|
|
118
|
+
| `animated` | `boolean` | `false` | If `true`, lines will animate (opacity pulse/shake). |
|
|
119
|
+
| `animationSpeed`| `number` | `1` | Speed multiplier for the animation. |
|
|
120
|
+
| `className` | `string` | `''` | Additional CSS classes. |
|
|
121
|
+
| `style` | `CSSProperties` | `{}` | Inline styles. |
|
|
122
|
+
|
|
123
|
+
#### Example: Intense Action
|
|
124
|
+
```tsx
|
|
125
|
+
<SpeedLines
|
|
126
|
+
lineCount={120}
|
|
127
|
+
color="red"
|
|
128
|
+
innerRadius={10}
|
|
129
|
+
animated={true}
|
|
130
|
+
animationSpeed={1.5}
|
|
131
|
+
/>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Storybook
|
|
135
|
+
|
|
136
|
+
We use Storybook for development and testing.
|
|
137
|
+
|
|
138
|
+
1. Clone the repository
|
|
139
|
+
2. Install dependencies: `npm install`
|
|
140
|
+
3. Run Storybook:
|
|
141
|
+
```bash
|
|
142
|
+
npm run storybook
|
|
143
|
+
```
|
|
144
|
+
4. Open [http://localhost:6006](http://localhost:6006) to view the components.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
Contributions are welcome!
|
|
149
|
+
|
|
150
|
+
1. Clone the repo:
|
|
151
|
+
```bash
|
|
152
|
+
git clone https://github.com/erutobusiness/react-manga-effects.git
|
|
153
|
+
cd react-manga-effects
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. Install dependencies:
|
|
157
|
+
```bash
|
|
158
|
+
npm install
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
3. Start development server (Storybook):
|
|
162
|
+
```bash
|
|
163
|
+
npm run storybook
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT © [erutobusiness](https://github.com/erutobusiness)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './IrisWipe';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useAnimationLoop: (callback: (deltaTime: number) => void, isActive: boolean) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SpeedLines';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Point } from '../../../types';
|
|
2
|
+
import { Line, SpeedLinesConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare const generateLines: (config: SpeedLinesConfig) => Line[];
|
|
5
|
+
export declare const drawLine: (ctx: CanvasRenderingContext2D, line: Line, center: Point, maxRadius: number, innerRadiusPct: number, // 0-100
|
|
6
|
+
color: string, width: number, height: number, time?: number, animated?: boolean, animationSpeed?: number) => void;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const I=require("react/jsx-runtime"),a=require("react"),$=({text:e="Manga Effect Placeholder"})=>I.jsx("div",{style:{padding:"2rem",border:"4px dashed #333",textAlign:"center",fontFamily:"sans-serif",fontWeight:"bold",backgroundColor:"#f0f0f0"},children:e}),j=({children:e,isOpen:s,center:n={x:50,y:50},duration:c=500,easing:i="easeInOut",onComplete:o,className:t="",style:r})=>{const E=a.useRef(null);let p="ease-in-out";if(typeof i=="string")switch(i){case"linear":p="linear";break;case"easeIn":p="ease-in";break;case"easeOut":p="ease-out";break;case"easeInOut":p="ease-in-out";break;default:p=i}const u=M=>{M.propertyName==="clip-path"&&o&&o()},y={"--iris-cx":`${n.x}%`,"--iris-cy":`${n.y}%`,"--iris-duration":`${c}ms`,"--iris-easing":p,...r};return I.jsx("div",{ref:E,className:`iris-wipe ${t}`,"data-state":s?"open":"closed",style:y,onTransitionEnd:u,children:e})},D=(e,s)=>{const n=a.useRef(),c=a.useRef(),i=o=>{if(c.current!==void 0){const t=o-c.current;e(t)}c.current=o,n.current=requestAnimationFrame(i)};a.useEffect(()=>(s?n.current=requestAnimationFrame(i):(n.current&&cancelAnimationFrame(n.current),c.current=void 0),()=>{n.current&&cancelAnimationFrame(n.current)}),[s,e])},W=(e,s,n,c)=>{const i=Math.cos(s),o=Math.sin(s);let t=1/0;if(Math.abs(i)>1e-4)if(i>0){const r=(n-e.x)/i;r>=0&&(t=Math.min(t,r))}else{const r=(0-e.x)/i;r>=0&&(t=Math.min(t,r))}if(Math.abs(o)>1e-4)if(o>0){const r=(c-e.y)/o;r>=0&&(t=Math.min(t,r))}else{const r=(0-e.y)/o;r>=0&&(t=Math.min(t,r))}return t===1/0?0:t},q=e=>Array.from({length:e.lineCount},()=>({angle:Math.random()*Math.PI*2,length:(e.minLength+Math.random()*(e.maxLength-e.minLength))/100,width:2+Math.random()*8,opacity:.6+Math.random()*.4,pulseOffset:Math.random()*Math.PI*2,pulseSpeed:2+Math.random()*3})),F=(e,s,n,c,i,o,t,r,E=0,p=!1,u=1)=>{const y=c*(i/100);let M=s.opacity;if(p){const P=.5+(Math.sin(E*s.pulseSpeed*u+s.pulseOffset)+1)*.25;M*=P}const m=W(n,s.angle,t,r)+2;let g=m-m*s.length;if(g<y&&(g=y),g>=m)return;const T=Math.cos(s.angle),R=Math.sin(s.angle),b=n.x+T*m,h=n.y+R*m,l=n.x+T*g,f=n.y+R*g,d=-R,w=T,x=s.width/2;e.beginPath(),e.moveTo(b+d*x,h+w*x),e.lineTo(b-d*x,h-w*x),e.lineTo(l,f),e.closePath(),e.fillStyle=o,e.globalAlpha=M,e.fill(),e.globalAlpha=1},C=({center:e={x:50,y:50},lineCount:s=60,color:n="rgba(0, 0, 0, 0.6)",minLength:c=10,maxLength:i=30,innerRadius:o=0,animated:t=!1,animationSpeed:r=1,className:E="",style:p})=>{const u=a.useRef(null),y=a.useRef(null),M=a.useRef([]),S=a.useRef(e),m=a.useRef(0),[g,T]=a.useState({width:0,height:0});a.useEffect(()=>{S.current=e},[e.x,e.y]),a.useEffect(()=>{M.current=q({lineCount:s,minLength:c,maxLength:i})},[s,c,i]);const R=a.useCallback(()=>{if(y.current&&u.current){const{clientWidth:h,clientHeight:l}=y.current,f=window.devicePixelRatio||1;if(u.current.width!==h*f||u.current.height!==l*f){u.current.width=h*f,u.current.height=l*f,u.current.style.width=`${h}px`,u.current.style.height=`${l}px`;const d=u.current.getContext("2d");d&&d.scale(f,f),T({width:h,height:l})}}},[]);a.useEffect(()=>{R();const h=new ResizeObserver(R);return y.current&&h.observe(y.current),()=>h.disconnect()},[R]);const b=a.useCallback(h=>{const l=u.current,f=y.current,d=l==null?void 0:l.getContext("2d");if(!l||!f||!d||g.width===0)return;d.save(),d.setTransform(1,0,0,1,0,0),d.clearRect(0,0,l.width,l.height),d.restore();const w=f.clientWidth,x=f.clientHeight,v={x:S.current.x/100*w,y:S.current.y/100*x},P=Math.max(v.x,w-v.x),A=Math.max(v.y,x-v.y),O=Math.hypot(P,A);t&&(m.current+=h*.001),M.current.forEach(k=>{F(d,k,v,O,o,n,w,x,m.current,t,r)})},[n,o,t,r,g]);return D(b,t),a.useEffect(()=>{t||b(0)},[b,t,s,c,i,o,n,g]),I.jsx("div",{ref:y,className:`speed-lines-container ${E}`,style:{width:"100%",height:"100%",position:"relative",overflow:"hidden",pointerEvents:"none",...p},children:I.jsx("canvas",{ref:u,style:{width:"100%",height:"100%",display:"block"}})})};exports.IrisWipe=j;exports.Placeholder=$;exports.SpeedLines=C;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { jsx as S } from "react/jsx-runtime";
|
|
2
|
+
import F, { useRef as M, useEffect as A, useCallback as P } from "react";
|
|
3
|
+
const H = ({ text: t = "Manga Effect Placeholder" }) => /* @__PURE__ */ S("div", { style: {
|
|
4
|
+
padding: "2rem",
|
|
5
|
+
border: "4px dashed #333",
|
|
6
|
+
textAlign: "center",
|
|
7
|
+
fontFamily: "sans-serif",
|
|
8
|
+
fontWeight: "bold",
|
|
9
|
+
backgroundColor: "#f0f0f0"
|
|
10
|
+
}, children: t }), j = ({
|
|
11
|
+
children: t,
|
|
12
|
+
isOpen: s,
|
|
13
|
+
center: n = { x: 50, y: 50 },
|
|
14
|
+
duration: a = 500,
|
|
15
|
+
easing: i = "easeInOut",
|
|
16
|
+
onComplete: o,
|
|
17
|
+
className: e = "",
|
|
18
|
+
style: r
|
|
19
|
+
}) => {
|
|
20
|
+
const E = M(null);
|
|
21
|
+
let p = "ease-in-out";
|
|
22
|
+
if (typeof i == "string")
|
|
23
|
+
switch (i) {
|
|
24
|
+
case "linear":
|
|
25
|
+
p = "linear";
|
|
26
|
+
break;
|
|
27
|
+
case "easeIn":
|
|
28
|
+
p = "ease-in";
|
|
29
|
+
break;
|
|
30
|
+
case "easeOut":
|
|
31
|
+
p = "ease-out";
|
|
32
|
+
break;
|
|
33
|
+
case "easeInOut":
|
|
34
|
+
p = "ease-in-out";
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
p = i;
|
|
38
|
+
}
|
|
39
|
+
const c = (x) => {
|
|
40
|
+
x.propertyName === "clip-path" && o && o();
|
|
41
|
+
}, f = {
|
|
42
|
+
"--iris-cx": `${n.x}%`,
|
|
43
|
+
"--iris-cy": `${n.y}%`,
|
|
44
|
+
"--iris-duration": `${a}ms`,
|
|
45
|
+
"--iris-easing": p,
|
|
46
|
+
...r
|
|
47
|
+
};
|
|
48
|
+
return /* @__PURE__ */ S(
|
|
49
|
+
"div",
|
|
50
|
+
{
|
|
51
|
+
ref: E,
|
|
52
|
+
className: `iris-wipe ${e}`,
|
|
53
|
+
"data-state": s ? "open" : "closed",
|
|
54
|
+
style: f,
|
|
55
|
+
onTransitionEnd: c,
|
|
56
|
+
children: t
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}, W = (t, s) => {
|
|
60
|
+
const n = M(), a = M(), i = (o) => {
|
|
61
|
+
if (a.current !== void 0) {
|
|
62
|
+
const e = o - a.current;
|
|
63
|
+
t(e);
|
|
64
|
+
}
|
|
65
|
+
a.current = o, n.current = requestAnimationFrame(i);
|
|
66
|
+
};
|
|
67
|
+
A(() => (s ? n.current = requestAnimationFrame(i) : (n.current && cancelAnimationFrame(n.current), a.current = void 0), () => {
|
|
68
|
+
n.current && cancelAnimationFrame(n.current);
|
|
69
|
+
}), [s, t]);
|
|
70
|
+
}, q = (t, s, n, a) => {
|
|
71
|
+
const i = Math.cos(s), o = Math.sin(s);
|
|
72
|
+
let e = 1 / 0;
|
|
73
|
+
if (Math.abs(i) > 1e-4)
|
|
74
|
+
if (i > 0) {
|
|
75
|
+
const r = (n - t.x) / i;
|
|
76
|
+
r >= 0 && (e = Math.min(e, r));
|
|
77
|
+
} else {
|
|
78
|
+
const r = (0 - t.x) / i;
|
|
79
|
+
r >= 0 && (e = Math.min(e, r));
|
|
80
|
+
}
|
|
81
|
+
if (Math.abs(o) > 1e-4)
|
|
82
|
+
if (o > 0) {
|
|
83
|
+
const r = (a - t.y) / o;
|
|
84
|
+
r >= 0 && (e = Math.min(e, r));
|
|
85
|
+
} else {
|
|
86
|
+
const r = (0 - t.y) / o;
|
|
87
|
+
r >= 0 && (e = Math.min(e, r));
|
|
88
|
+
}
|
|
89
|
+
return e === 1 / 0 ? 0 : e;
|
|
90
|
+
}, z = (t) => Array.from({ length: t.lineCount }, () => ({
|
|
91
|
+
angle: Math.random() * Math.PI * 2,
|
|
92
|
+
// Convert percentage 0-100 to 0-1
|
|
93
|
+
length: (t.minLength + Math.random() * (t.maxLength - t.minLength)) / 100,
|
|
94
|
+
// Thick at start (outer edge), tapers to thin
|
|
95
|
+
width: 2 + Math.random() * 8,
|
|
96
|
+
opacity: 0.6 + Math.random() * 0.4,
|
|
97
|
+
pulseOffset: Math.random() * Math.PI * 2,
|
|
98
|
+
// Increased pulse speed to be more noticeable (was 0.5-1.0, now 2.0-5.0)
|
|
99
|
+
pulseSpeed: 2 + Math.random() * 3
|
|
100
|
+
})), C = (t, s, n, a, i, o, e, r, E = 0, p = !1, c = 1) => {
|
|
101
|
+
const f = a * (i / 100);
|
|
102
|
+
let x = s.opacity;
|
|
103
|
+
if (p) {
|
|
104
|
+
const O = 0.5 + (Math.sin(E * s.pulseSpeed * c + s.pulseOffset) + 1) * 0.25;
|
|
105
|
+
x *= O;
|
|
106
|
+
}
|
|
107
|
+
const m = q(n, s.angle, e, r) + 2;
|
|
108
|
+
let y = m - m * s.length;
|
|
109
|
+
if (y < f && (y = f), y >= m) return;
|
|
110
|
+
const T = Math.cos(s.angle), w = Math.sin(s.angle), b = n.x + T * m, l = n.y + w * m, h = n.x + T * y, u = n.y + w * y, d = -w, v = T, g = s.width / 2;
|
|
111
|
+
t.beginPath(), t.moveTo(b + d * g, l + v * g), t.lineTo(b - d * g, l - v * g), t.lineTo(h, u), t.closePath(), t.fillStyle = o, t.globalAlpha = x, t.fill(), t.globalAlpha = 1;
|
|
112
|
+
}, X = ({
|
|
113
|
+
center: t = { x: 50, y: 50 },
|
|
114
|
+
lineCount: s = 60,
|
|
115
|
+
color: n = "rgba(0, 0, 0, 0.6)",
|
|
116
|
+
minLength: a = 10,
|
|
117
|
+
// Percentage
|
|
118
|
+
maxLength: i = 30,
|
|
119
|
+
// Percentage
|
|
120
|
+
innerRadius: o = 0,
|
|
121
|
+
// Percentage
|
|
122
|
+
animated: e = !1,
|
|
123
|
+
animationSpeed: r = 1,
|
|
124
|
+
className: E = "",
|
|
125
|
+
style: p
|
|
126
|
+
}) => {
|
|
127
|
+
const c = M(null), f = M(null), x = M([]), I = M(t), m = M(0), [y, T] = F.useState({ width: 0, height: 0 });
|
|
128
|
+
A(() => {
|
|
129
|
+
I.current = t;
|
|
130
|
+
}, [t.x, t.y]), A(() => {
|
|
131
|
+
x.current = z({
|
|
132
|
+
lineCount: s,
|
|
133
|
+
minLength: a,
|
|
134
|
+
maxLength: i
|
|
135
|
+
});
|
|
136
|
+
}, [s, a, i]);
|
|
137
|
+
const w = P(() => {
|
|
138
|
+
if (f.current && c.current) {
|
|
139
|
+
const { clientWidth: l, clientHeight: h } = f.current, u = window.devicePixelRatio || 1;
|
|
140
|
+
if (c.current.width !== l * u || c.current.height !== h * u) {
|
|
141
|
+
c.current.width = l * u, c.current.height = h * u, c.current.style.width = `${l}px`, c.current.style.height = `${h}px`;
|
|
142
|
+
const d = c.current.getContext("2d");
|
|
143
|
+
d && d.scale(u, u), T({ width: l, height: h });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, []);
|
|
147
|
+
A(() => {
|
|
148
|
+
w();
|
|
149
|
+
const l = new ResizeObserver(w);
|
|
150
|
+
return f.current && l.observe(f.current), () => l.disconnect();
|
|
151
|
+
}, [w]);
|
|
152
|
+
const b = P((l) => {
|
|
153
|
+
const h = c.current, u = f.current, d = h == null ? void 0 : h.getContext("2d");
|
|
154
|
+
if (!h || !u || !d || y.width === 0) return;
|
|
155
|
+
d.save(), d.setTransform(1, 0, 0, 1, 0, 0), d.clearRect(0, 0, h.width, h.height), d.restore();
|
|
156
|
+
const v = u.clientWidth, g = u.clientHeight, R = {
|
|
157
|
+
x: I.current.x / 100 * v,
|
|
158
|
+
y: I.current.y / 100 * g
|
|
159
|
+
}, O = Math.max(R.x, v - R.x), $ = Math.max(R.y, g - R.y), k = Math.hypot(O, $);
|
|
160
|
+
e && (m.current += l * 1e-3), x.current.forEach((D) => {
|
|
161
|
+
C(
|
|
162
|
+
d,
|
|
163
|
+
D,
|
|
164
|
+
R,
|
|
165
|
+
k,
|
|
166
|
+
o,
|
|
167
|
+
n,
|
|
168
|
+
v,
|
|
169
|
+
g,
|
|
170
|
+
m.current,
|
|
171
|
+
e,
|
|
172
|
+
r
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
}, [n, o, e, r, y]);
|
|
176
|
+
return W(b, e), A(() => {
|
|
177
|
+
e || b(0);
|
|
178
|
+
}, [b, e, s, a, i, o, n, y]), /* @__PURE__ */ S(
|
|
179
|
+
"div",
|
|
180
|
+
{
|
|
181
|
+
ref: f,
|
|
182
|
+
className: `speed-lines-container ${E}`,
|
|
183
|
+
style: {
|
|
184
|
+
width: "100%",
|
|
185
|
+
height: "100%",
|
|
186
|
+
position: "relative",
|
|
187
|
+
overflow: "hidden",
|
|
188
|
+
pointerEvents: "none",
|
|
189
|
+
...p
|
|
190
|
+
},
|
|
191
|
+
children: /* @__PURE__ */ S(
|
|
192
|
+
"canvas",
|
|
193
|
+
{
|
|
194
|
+
ref: c,
|
|
195
|
+
style: { width: "100%", height: "100%", display: "block" }
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
export {
|
|
202
|
+
j as IrisWipe,
|
|
203
|
+
H as Placeholder,
|
|
204
|
+
X as SpeedLines
|
|
205
|
+
};
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.iris-wipe{--iris-cx: 50%;--iris-cy: 50%;--iris-duration: .5s;--iris-easing: ease-in-out;width:100%;height:100%;position:relative;overflow:hidden;will-change:clip-path;transition:clip-path var(--iris-duration) var(--iris-easing)}.iris-wipe[data-state=open]{clip-path:circle(150% at var(--iris-cx) var(--iris-cy))}.iris-wipe[data-state=closed]{clip-path:circle(0% at var(--iris-cx) var(--iris-cy))}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
|
|
3
|
+
export type { PlaceholderProps } from '../components/Placeholder/Placeholder';
|
|
4
|
+
/**
|
|
5
|
+
* Represents a 2D point with coordinates.
|
|
6
|
+
*/
|
|
7
|
+
export interface Point {
|
|
8
|
+
/** X coordinate */
|
|
9
|
+
x: number;
|
|
10
|
+
/** Y coordinate */
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A function that maps a time value t (0-1) to an eased value.
|
|
15
|
+
* @param t - The time value between 0 and 1.
|
|
16
|
+
* @returns The eased value.
|
|
17
|
+
*/
|
|
18
|
+
export type EasingFunction = (t: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* Preset easing names available in the library.
|
|
21
|
+
*/
|
|
22
|
+
export type EasingPreset = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
|
23
|
+
/**
|
|
24
|
+
* Props for the IrisWipe component.
|
|
25
|
+
*/
|
|
26
|
+
export interface IrisWipeProps {
|
|
27
|
+
/** The content to be revealed/hidden */
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
/** Controls the open/close state of the iris. True for open, False for closed. */
|
|
30
|
+
isOpen: boolean;
|
|
31
|
+
/** Center point of the iris (0-100 percentage). Default: { x: 50, y: 50 } */
|
|
32
|
+
center?: Point;
|
|
33
|
+
/** Animation duration in milliseconds. Default: 500 */
|
|
34
|
+
duration?: number;
|
|
35
|
+
/** Easing function or preset name for the animation. Default: 'easeInOut' */
|
|
36
|
+
easing?: EasingPreset | EasingFunction;
|
|
37
|
+
/** Callback fired when animation completes */
|
|
38
|
+
onComplete?: () => void;
|
|
39
|
+
/** Additional CSS class for the container */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Additional inline styles for the container */
|
|
42
|
+
style?: React.CSSProperties;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Props for the SpeedLines component.
|
|
46
|
+
*/
|
|
47
|
+
export interface SpeedLinesProps {
|
|
48
|
+
/** Center point of the speed lines origin (0-100 percentage). Default: { x: 50, y: 50 } */
|
|
49
|
+
center?: Point;
|
|
50
|
+
/** Number of lines to render. Default: 60 */
|
|
51
|
+
lineCount?: number;
|
|
52
|
+
/** Line color (CSS color string). Default: 'rgba(0, 0, 0, 0.6)' */
|
|
53
|
+
color?: string;
|
|
54
|
+
/** Minimum line length as percentage of radius (0-100). Default: 10 */
|
|
55
|
+
minLength?: number;
|
|
56
|
+
/** Maximum line length as percentage of radius (0-100). Default: 30 */
|
|
57
|
+
maxLength?: number;
|
|
58
|
+
/** Radius of the clear center area as percentage of container size (0-100). Default: 0 */
|
|
59
|
+
innerRadius?: number;
|
|
60
|
+
/** Whether to animate the lines (subtle pulse). Default: false */
|
|
61
|
+
animated?: boolean;
|
|
62
|
+
/** Speed of the pulse animation if animated. Default: 1 */
|
|
63
|
+
animationSpeed?: number;
|
|
64
|
+
/** Additional CSS class for the container */
|
|
65
|
+
className?: string;
|
|
66
|
+
/** Additional inline styles for the container */
|
|
67
|
+
style?: React.CSSProperties;
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EasingFunction } from '../types';
|
|
2
|
+
|
|
3
|
+
export declare const linear: EasingFunction;
|
|
4
|
+
export declare const easeIn: EasingFunction;
|
|
5
|
+
export declare const easeOut: EasingFunction;
|
|
6
|
+
export declare const easeInOut: EasingFunction;
|
|
7
|
+
export declare const easings: {
|
|
8
|
+
linear: EasingFunction;
|
|
9
|
+
easeIn: EasingFunction;
|
|
10
|
+
easeOut: EasingFunction;
|
|
11
|
+
easeInOut: EasingFunction;
|
|
12
|
+
};
|
|
13
|
+
export declare const getEasing: (easing?: string | EasingFunction) => EasingFunction;
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-manga-effects",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight React components for manga/anime-style visual effects - iris wipes and speed lines",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"manga",
|
|
8
|
+
"anime",
|
|
9
|
+
"effects",
|
|
10
|
+
"iris-wipe",
|
|
11
|
+
"speed-lines",
|
|
12
|
+
"focus-lines",
|
|
13
|
+
"transition",
|
|
14
|
+
"animation",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/erutobusiness/react-manga-effects#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/erutobusiness/react-manga-effects/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/erutobusiness/react-manga-effects.git"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "erutobusiness",
|
|
27
|
+
"sideEffects": [
|
|
28
|
+
"*.css"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"type": "module",
|
|
36
|
+
"main": "./dist/index.cjs",
|
|
37
|
+
"module": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"import": "./dist/index.js",
|
|
42
|
+
"require": "./dist/index.cjs",
|
|
43
|
+
"types": "./dist/index.d.ts"
|
|
44
|
+
},
|
|
45
|
+
"./style.css": "./dist/style.css"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "vite",
|
|
49
|
+
"build": "tsc && vite build",
|
|
50
|
+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
51
|
+
"preview": "vite preview",
|
|
52
|
+
"storybook": "storybook dev -p 6006",
|
|
53
|
+
"build-storybook": "storybook build",
|
|
54
|
+
"test": "vitest"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
58
|
+
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@storybook/react": "^8.0.0",
|
|
62
|
+
"@storybook/react-vite": "^8.0.0",
|
|
63
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
64
|
+
"@testing-library/react": "^16.3.1",
|
|
65
|
+
"@types/node": "^20.0.0",
|
|
66
|
+
"@types/react": "^18.2.0",
|
|
67
|
+
"@types/react-dom": "^18.2.0",
|
|
68
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
69
|
+
"jsdom": "^27.0.1",
|
|
70
|
+
"react": "^18.2.0",
|
|
71
|
+
"react-dom": "^18.2.0",
|
|
72
|
+
"storybook": "^8.0.0",
|
|
73
|
+
"typescript": "^5.2.0",
|
|
74
|
+
"vite": "^5.0.0",
|
|
75
|
+
"vite-plugin-dts": "^3.0.0",
|
|
76
|
+
"vitest": "^1.0.0"
|
|
77
|
+
}
|
|
78
|
+
}
|