@matteodicristofalo/text-animations 1.0.0 → 1.0.2
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/README.md +1 -1
- package/dist/components/split-text-reveal/split-test-reveal.types.d.ts +11 -0
- package/dist/components/split-text-reveal/split-text-reveal.d.ts +3 -0
- package/dist/components/split-text-reveal/split-text-reveal.stories.d.ts +26 -0
- package/dist/components/split-text-rotate/split-text-rotate.d.ts +3 -0
- package/dist/components/split-text-rotate/split-text-rotate.stories.d.ts +22 -0
- package/dist/components/split-text-rotate/split-text-rotate.types.d.ts +9 -0
- package/dist/hooks/intersection-overser/intersection-observer.d.ts +6 -0
- package/dist/index.css +48 -0
- package/dist/index.js +146 -0
- package/dist/utils/numbers.d.ts +1 -0
- package/dist/utils/text.d.ts +4 -0
- package/package.json +4 -1
- package/.storybook/main.ts +0 -22
- package/.storybook/preview.ts +0 -14
- package/rollup.config.mjs +0 -21
- package/src/components/split-text-reveal/split-test-reveal.types.ts +0 -12
- package/src/components/split-text-reveal/split-text-reveal.module.scss +0 -26
- package/src/components/split-text-reveal/split-text-reveal.stories.ts +0 -34
- package/src/components/split-text-reveal/split-text-reveal.tsx +0 -61
- package/src/components/split-text-rotate/split-text-rotate.module.scss +0 -34
- package/src/components/split-text-rotate/split-text-rotate.stories.ts +0 -29
- package/src/components/split-text-rotate/split-text-rotate.tsx +0 -60
- package/src/components/split-text-rotate/split-text-rotate.types.ts +0 -9
- package/src/global.d.ts +0 -4
- package/src/hooks/intersection-overser/intersection-observer.ts +0 -41
- package/src/utils/numbers.ts +0 -3
- package/src/utils/text.ts +0 -19
- package/tsconfig.json +0 -13
- /package/{src/components/index.ts → dist/components/index.d.ts} +0 -0
- /package/{src/hooks/index.ts → dist/hooks/index.d.ts} +0 -0
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import { SplitTextReveal } from "./split-text-reveal";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof SplitTextReveal;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
argTypes: {
|
|
10
|
+
text: {
|
|
11
|
+
control: "text";
|
|
12
|
+
};
|
|
13
|
+
splitType: {
|
|
14
|
+
control: "radio";
|
|
15
|
+
options: string[];
|
|
16
|
+
};
|
|
17
|
+
revealOptions: {
|
|
18
|
+
control: {
|
|
19
|
+
type: "object";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof meta>;
|
|
26
|
+
export declare const Default: Story;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StoryObj } from "@storybook/react";
|
|
2
|
+
import { SplitTextRotate } from "./split-text-rotate";
|
|
3
|
+
declare const meta: {
|
|
4
|
+
title: string;
|
|
5
|
+
component: typeof SplitTextRotate;
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
argTypes: {
|
|
10
|
+
text: {
|
|
11
|
+
control: "text";
|
|
12
|
+
};
|
|
13
|
+
rotateOptions: {
|
|
14
|
+
control: {
|
|
15
|
+
type: "object";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
export declare const Default: Story;
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.split-text-reveal-module_text__-9O5o {
|
|
2
|
+
position: relative;
|
|
3
|
+
}
|
|
4
|
+
.split-text-reveal-module_text__-9O5o .split-text-reveal-module_is-hidden__2J8-i {
|
|
5
|
+
opacity: 0;
|
|
6
|
+
}
|
|
7
|
+
.split-text-reveal-module_text__-9O5o .split-text-reveal-module_container__mkD-P {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
.split-text-reveal-module_text__-9O5o .split-text-reveal-module_container__mkD-P span {
|
|
12
|
+
min-width: 0.25em;
|
|
13
|
+
transform: translateY(100%);
|
|
14
|
+
transition-delay: var(--var-delay);
|
|
15
|
+
transition-duration: var(--var-duration);
|
|
16
|
+
transition-property: transform;
|
|
17
|
+
transition-timing-function: ease-out;
|
|
18
|
+
will-change: transform;
|
|
19
|
+
}
|
|
20
|
+
.split-text-reveal-module_text__-9O5o.split-text-reveal-module_animate__y6o78 .split-text-reveal-module_container__mkD-P span {
|
|
21
|
+
transform: translateY(0);
|
|
22
|
+
}
|
|
23
|
+
.split-text-rotate-module_text__xzran {
|
|
24
|
+
display: block;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
.split-text-rotate-module_text__xzran .split-text-rotate-module_container__EAJkE {
|
|
29
|
+
display: flex;
|
|
30
|
+
}
|
|
31
|
+
.split-text-rotate-module_text__xzran .split-text-rotate-module_container__EAJkE:nth-of-type(2) {
|
|
32
|
+
position: absolute;
|
|
33
|
+
top: 0;
|
|
34
|
+
transform: translateY(100%);
|
|
35
|
+
}
|
|
36
|
+
.split-text-rotate-module_text__xzran .split-text-rotate-module_container__EAJkE .split-text-rotate-module_char__PYJ-k {
|
|
37
|
+
display: inline-block;
|
|
38
|
+
min-width: 0.25em;
|
|
39
|
+
transition-delay: var(--var-delay-out);
|
|
40
|
+
transition-duration: var(--var-duration);
|
|
41
|
+
transition-property: transform;
|
|
42
|
+
transition-timing-function: ease-out;
|
|
43
|
+
will-change: transform;
|
|
44
|
+
}
|
|
45
|
+
.split-text-rotate-module_text__xzran:hover .split-text-rotate-module_container__EAJkE .split-text-rotate-module_char__PYJ-k {
|
|
46
|
+
transform: translateY(-100%);
|
|
47
|
+
transition-delay: var(--var-delay-in);
|
|
48
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
function round(num, digits) {
|
|
4
|
+
return parseFloat(num.toFixed(digits));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sentences(text) {
|
|
8
|
+
return segment(text, "sentence");
|
|
9
|
+
}
|
|
10
|
+
function words(text) {
|
|
11
|
+
return segment(text, "word");
|
|
12
|
+
}
|
|
13
|
+
function characters(text) {
|
|
14
|
+
return segment(text, "grapheme");
|
|
15
|
+
}
|
|
16
|
+
function segment(text, granularity) {
|
|
17
|
+
var segmenter = new Intl.Segmenter(undefined, { granularity: granularity });
|
|
18
|
+
return Array.from(segmenter.segment(text), function (segment) { return segment.segment; });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/******************************************************************************
|
|
22
|
+
Copyright (c) Microsoft Corporation.
|
|
23
|
+
|
|
24
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
25
|
+
purpose with or without fee is hereby granted.
|
|
26
|
+
|
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
28
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
29
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
30
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
31
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
32
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
33
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
34
|
+
***************************************************************************** */
|
|
35
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
var __assign = function() {
|
|
39
|
+
__assign = Object.assign || function __assign(t) {
|
|
40
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
41
|
+
s = arguments[i];
|
|
42
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
43
|
+
}
|
|
44
|
+
return t;
|
|
45
|
+
};
|
|
46
|
+
return __assign.apply(this, arguments);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function useIntersectionObserver(ref, options) {
|
|
55
|
+
var _a = useState(false), isIntersecting = _a[0], setIntersecting = _a[1];
|
|
56
|
+
useEffect(function () {
|
|
57
|
+
if (!ref.current)
|
|
58
|
+
return;
|
|
59
|
+
var observer = new IntersectionObserver(function (_a) {
|
|
60
|
+
var entry = _a[0];
|
|
61
|
+
if (options === null || options === void 0 ? void 0 : options.once) {
|
|
62
|
+
if (entry.isIntersecting) {
|
|
63
|
+
setIntersecting(true);
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
setIntersecting(entry.isIntersecting);
|
|
69
|
+
}
|
|
70
|
+
}, __assign({ threshold: 0.75 }, options));
|
|
71
|
+
observer.observe(ref.current);
|
|
72
|
+
return function () { return observer.disconnect(); };
|
|
73
|
+
}, [ref, options]);
|
|
74
|
+
return isIntersecting;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n}
|
|
78
|
+
|
|
79
|
+
var styles$1 = {"text":"split-text-reveal-module_text__-9O5o","is-hidden":"split-text-reveal-module_is-hidden__2J8-i","container":"split-text-reveal-module_container__mkD-P","animate":"split-text-reveal-module_animate__y6o78"};
|
|
80
|
+
|
|
81
|
+
var DEFAULT_DURATION$1 = 0.5;
|
|
82
|
+
var DEFAULT_STAGGER$1 = 0.01;
|
|
83
|
+
function SplitTextReveal(_a) {
|
|
84
|
+
var _b;
|
|
85
|
+
var text = _a.text, _c = _a.splitType, splitType = _c === void 0 ? "char" : _c, revealOptions = _a.revealOptions;
|
|
86
|
+
var _d = useState(false), hasBeenMounted = _d[0], setHasBeenMounted = _d[1];
|
|
87
|
+
var ref = useRef(null);
|
|
88
|
+
var memoizedOptions = useMemo(function () { return ({ once: true, threshold: 0.1 }); }, []);
|
|
89
|
+
var isInView = useIntersectionObserver(ref, memoizedOptions);
|
|
90
|
+
var transitionDuration = (revealOptions === null || revealOptions === void 0 ? void 0 : revealOptions.duration) || DEFAULT_DURATION$1;
|
|
91
|
+
var transitionStagger = (revealOptions === null || revealOptions === void 0 ? void 0 : revealOptions.stagger) || DEFAULT_STAGGER$1;
|
|
92
|
+
var splittedText = useMemo(function () {
|
|
93
|
+
switch (splitType) {
|
|
94
|
+
case "sentence":
|
|
95
|
+
return sentences(text);
|
|
96
|
+
case "word":
|
|
97
|
+
return words(text);
|
|
98
|
+
case "char":
|
|
99
|
+
return characters(text);
|
|
100
|
+
}
|
|
101
|
+
}, [splitType, text]);
|
|
102
|
+
useEffect(function () {
|
|
103
|
+
setHasBeenMounted(true);
|
|
104
|
+
}, []);
|
|
105
|
+
return (React.createElement("span", { ref: ref, className: clsx(styles$1["text"], (_b = {}, _b[styles$1["animate"]] = isInView, _b)), style: {
|
|
106
|
+
"--var-duration": "".concat(transitionDuration, "s"),
|
|
107
|
+
} }, hasBeenMounted ? (splittedText.map(function (el, i) { return (React.createElement("span", { key: i, className: styles$1["container"] },
|
|
108
|
+
React.createElement("span", { style: {
|
|
109
|
+
"--var-delay": "".concat(round(transitionStagger * i, 3), "s"),
|
|
110
|
+
} }, el))); })) : (React.createElement("span", { className: styles$1["is-hidden"] }, text))));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var styles = {"text":"split-text-rotate-module_text__xzran","container":"split-text-rotate-module_container__EAJkE","char":"split-text-rotate-module_char__PYJ-k"};
|
|
114
|
+
|
|
115
|
+
var DEFAULT_DURATION = 0.25;
|
|
116
|
+
var DEFAULT_STAGGER = 0.025;
|
|
117
|
+
function SplitTextRotate(_a) {
|
|
118
|
+
var text = _a.text, rotateOptions = _a.rotateOptions;
|
|
119
|
+
var _b = useState(false), hasBeenMounted = _b[0], setHasBeenMounted = _b[1];
|
|
120
|
+
var splittedText = useMemo(function () { return characters(text); }, [text]);
|
|
121
|
+
var _c = useMemo(function () { return ({
|
|
122
|
+
transitionDuration: (rotateOptions === null || rotateOptions === void 0 ? void 0 : rotateOptions.duration) || DEFAULT_DURATION,
|
|
123
|
+
transitionStagger: (rotateOptions === null || rotateOptions === void 0 ? void 0 : rotateOptions.stagger) || DEFAULT_STAGGER,
|
|
124
|
+
}); }, [rotateOptions]), transitionDuration = _c.transitionDuration, transitionStagger = _c.transitionStagger;
|
|
125
|
+
var delays = useCallback(function (index) {
|
|
126
|
+
var noElementsZeroBased = splittedText.length - 1;
|
|
127
|
+
return {
|
|
128
|
+
in: round(transitionStagger * index, 3),
|
|
129
|
+
out: round(transitionStagger * (noElementsZeroBased - index), 3),
|
|
130
|
+
};
|
|
131
|
+
}, [splittedText, transitionStagger]);
|
|
132
|
+
useEffect(function () {
|
|
133
|
+
setHasBeenMounted(true);
|
|
134
|
+
}, []);
|
|
135
|
+
return (React.createElement("span", { className: styles["text"], style: {
|
|
136
|
+
"--var-duration": "".concat(transitionDuration, "s"),
|
|
137
|
+
} }, hasBeenMounted ? (Array.from({ length: 2 }).map(function (_, i) { return (React.createElement("span", { key: i, className: styles["container"] }, splittedText.map(function (char, i) {
|
|
138
|
+
var charDelays = delays(i);
|
|
139
|
+
return (React.createElement("span", { key: i, className: styles["char"], style: {
|
|
140
|
+
"--var-delay-in": "".concat(charDelays.in, "s"),
|
|
141
|
+
"--var-delay-out": "".concat(charDelays.out, "s"),
|
|
142
|
+
} }, char));
|
|
143
|
+
}))); })) : (React.createElement("span", null, text))));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { SplitTextReveal, SplitTextRotate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function round(num: number, digits: number): number;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sentences(text: string): string[];
|
|
2
|
+
export declare function words(text: string): string[];
|
|
3
|
+
export declare function characters(text: string): string[];
|
|
4
|
+
export declare function segment(text: string, granularity: "sentence" | "word" | "grapheme"): string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matteodicristofalo/text-animations",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "React library to animate text",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "rimraf dist && rollup -c",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
".": "./dist/index.js",
|
|
44
44
|
"./index.css": "./dist/index.css"
|
|
45
45
|
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist"
|
|
48
|
+
],
|
|
46
49
|
"author": "Matteo Di Cristofalo",
|
|
47
50
|
"license": "ISC",
|
|
48
51
|
"homepage": "https://github.com/matteodicristofalo/text-animations",
|
package/.storybook/main.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
-
import tsconfigPaths from "vite-tsconfig-paths";
|
|
3
|
-
|
|
4
|
-
const config: StorybookConfig = {
|
|
5
|
-
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
6
|
-
addons: [
|
|
7
|
-
"@storybook/addon-onboarding",
|
|
8
|
-
"@storybook/addon-essentials",
|
|
9
|
-
"@chromatic-com/storybook",
|
|
10
|
-
"@storybook/addon-interactions",
|
|
11
|
-
],
|
|
12
|
-
framework: {
|
|
13
|
-
name: "@storybook/react-vite",
|
|
14
|
-
options: {},
|
|
15
|
-
},
|
|
16
|
-
viteFinal: async (config) => {
|
|
17
|
-
config.plugins?.push(tsconfigPaths());
|
|
18
|
-
return config;
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default config;
|
package/.storybook/preview.ts
DELETED
package/rollup.config.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
-
import typescript from '@rollup/plugin-typescript';
|
|
3
|
-
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
|
4
|
-
import postCss from 'rollup-plugin-postcss';
|
|
5
|
-
|
|
6
|
-
export default {
|
|
7
|
-
input: 'src/index.ts',
|
|
8
|
-
output: {
|
|
9
|
-
dir: 'dist',
|
|
10
|
-
format: 'es',
|
|
11
|
-
},
|
|
12
|
-
plugins: [
|
|
13
|
-
postCss({
|
|
14
|
-
extract: true,
|
|
15
|
-
modules: true,
|
|
16
|
-
}),
|
|
17
|
-
typescript(),
|
|
18
|
-
peerDepsExternal(),
|
|
19
|
-
resolve(),
|
|
20
|
-
],
|
|
21
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
.text {
|
|
2
|
-
position: relative;
|
|
3
|
-
|
|
4
|
-
.container {
|
|
5
|
-
display: inline-flex;
|
|
6
|
-
overflow: hidden;
|
|
7
|
-
|
|
8
|
-
span {
|
|
9
|
-
min-width: 0.25em;
|
|
10
|
-
transform: translateY(100%);
|
|
11
|
-
transition-delay: var(--var-delay);
|
|
12
|
-
transition-duration: var(--var-duration);
|
|
13
|
-
transition-property: transform;
|
|
14
|
-
transition-timing-function: ease-out;
|
|
15
|
-
will-change: transform;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
&.animate {
|
|
20
|
-
.container {
|
|
21
|
-
span {
|
|
22
|
-
transform: translateY(0);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { SplitTextReveal } from "./split-text-reveal";
|
|
3
|
-
|
|
4
|
-
const meta = {
|
|
5
|
-
title: "SplitTextReveal",
|
|
6
|
-
component: SplitTextReveal,
|
|
7
|
-
parameters: {
|
|
8
|
-
layout: "centered",
|
|
9
|
-
},
|
|
10
|
-
argTypes: {
|
|
11
|
-
text: {
|
|
12
|
-
control: "text",
|
|
13
|
-
},
|
|
14
|
-
splitType: {
|
|
15
|
-
control: "radio",
|
|
16
|
-
options: ["char", "word", "sentence"],
|
|
17
|
-
},
|
|
18
|
-
revealOptions: {
|
|
19
|
-
control: {
|
|
20
|
-
type: "object",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
} satisfies Meta<typeof SplitTextReveal>;
|
|
25
|
-
|
|
26
|
-
export default meta;
|
|
27
|
-
type Story = StoryObj<typeof meta>;
|
|
28
|
-
|
|
29
|
-
export const Default: Story = {
|
|
30
|
-
args: {
|
|
31
|
-
text: "Here is the revealed text",
|
|
32
|
-
splitType: "char",
|
|
33
|
-
},
|
|
34
|
-
};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useMemo, useRef } from "react";
|
|
4
|
-
import { round } from "@utils/numbers";
|
|
5
|
-
import { characters, sentences, words } from "@utils/text";
|
|
6
|
-
import { useIntersectionObserver } from "@hooks";
|
|
7
|
-
import { SplitTextRevealProps } from "./split-test-reveal.types";
|
|
8
|
-
import clsx from "clsx";
|
|
9
|
-
import styles from "./split-text-reveal.module.scss";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_DURATION = 0.5;
|
|
12
|
-
const DEFAULT_STAGGER = 0.01;
|
|
13
|
-
|
|
14
|
-
export function SplitTextReveal({
|
|
15
|
-
text,
|
|
16
|
-
splitType = "char",
|
|
17
|
-
revealOptions,
|
|
18
|
-
}: SplitTextRevealProps) {
|
|
19
|
-
const ref = useRef(null);
|
|
20
|
-
const memoizedOptions = useMemo(() => ({ once: true, threshold: 0.1 }), []);
|
|
21
|
-
const isInView = useIntersectionObserver(ref, memoizedOptions);
|
|
22
|
-
const transitionDuration = revealOptions?.duration || DEFAULT_DURATION;
|
|
23
|
-
const transitionStagger = revealOptions?.stagger || DEFAULT_STAGGER;
|
|
24
|
-
|
|
25
|
-
const splitFn = useMemo(() => {
|
|
26
|
-
switch (splitType) {
|
|
27
|
-
case "sentence":
|
|
28
|
-
return sentences;
|
|
29
|
-
case "word":
|
|
30
|
-
return words;
|
|
31
|
-
case "char":
|
|
32
|
-
return characters;
|
|
33
|
-
}
|
|
34
|
-
}, [splitType]);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<span
|
|
38
|
-
ref={ref}
|
|
39
|
-
className={clsx(styles["text"], { [styles["animate"]]: isInView })}
|
|
40
|
-
style={
|
|
41
|
-
{
|
|
42
|
-
"--var-duration": `${transitionDuration}s`,
|
|
43
|
-
} as React.CSSProperties
|
|
44
|
-
}
|
|
45
|
-
>
|
|
46
|
-
{splitFn(text).map((el, i) => (
|
|
47
|
-
<span key={i} className={styles["container"]}>
|
|
48
|
-
<span
|
|
49
|
-
style={
|
|
50
|
-
{
|
|
51
|
-
"--var-delay": `${round(transitionStagger * i, 3)}s`,
|
|
52
|
-
} as React.CSSProperties
|
|
53
|
-
}
|
|
54
|
-
>
|
|
55
|
-
{el}
|
|
56
|
-
</span>
|
|
57
|
-
</span>
|
|
58
|
-
))}
|
|
59
|
-
</span>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
.text {
|
|
2
|
-
display: block;
|
|
3
|
-
overflow: hidden;
|
|
4
|
-
position: relative;
|
|
5
|
-
|
|
6
|
-
.container {
|
|
7
|
-
display: flex;
|
|
8
|
-
|
|
9
|
-
&:nth-of-type(2) {
|
|
10
|
-
position: absolute;
|
|
11
|
-
top: 0;
|
|
12
|
-
transform: translateY(100%);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.char {
|
|
16
|
-
display: inline-block;
|
|
17
|
-
min-width: 0.25em;
|
|
18
|
-
transition-delay: var(--var-delay-out);
|
|
19
|
-
transition-duration: var(--var-duration);
|
|
20
|
-
transition-property: transform;
|
|
21
|
-
transition-timing-function: ease-out;
|
|
22
|
-
will-change: transform;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
&:hover {
|
|
27
|
-
.container {
|
|
28
|
-
.char {
|
|
29
|
-
transform: translateY(-100%);
|
|
30
|
-
transition-delay: var(--var-delay-in);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { SplitTextRotate } from "./split-text-rotate";
|
|
3
|
-
|
|
4
|
-
const meta = {
|
|
5
|
-
title: "SplitTextRotate",
|
|
6
|
-
component: SplitTextRotate,
|
|
7
|
-
parameters: {
|
|
8
|
-
layout: "centered",
|
|
9
|
-
},
|
|
10
|
-
argTypes: {
|
|
11
|
-
text: {
|
|
12
|
-
control: "text",
|
|
13
|
-
},
|
|
14
|
-
rotateOptions: {
|
|
15
|
-
control: {
|
|
16
|
-
type: "object",
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
} satisfies Meta<typeof SplitTextRotate>;
|
|
21
|
-
|
|
22
|
-
export default meta;
|
|
23
|
-
type Story = StoryObj<typeof meta>;
|
|
24
|
-
|
|
25
|
-
export const Default: Story = {
|
|
26
|
-
args: {
|
|
27
|
-
text: "Rotating text",
|
|
28
|
-
},
|
|
29
|
-
};
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useCallback } from "react";
|
|
4
|
-
import { round } from "@utils/numbers";
|
|
5
|
-
import { characters } from "@utils/text";
|
|
6
|
-
import { SplitTextRotateProps } from "./split-text-rotate.types";
|
|
7
|
-
import styles from "./split-text-rotate.module.scss";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_DURATION = 0.25;
|
|
10
|
-
const DEFAULT_STAGGER = 0.025;
|
|
11
|
-
|
|
12
|
-
export function SplitTextRotate({ text, rotateOptions }: SplitTextRotateProps) {
|
|
13
|
-
const transitionDuration = rotateOptions?.duration || DEFAULT_DURATION;
|
|
14
|
-
const transitionStagger = rotateOptions?.stagger || DEFAULT_STAGGER;
|
|
15
|
-
const splittedText = characters(text);
|
|
16
|
-
|
|
17
|
-
const delays = useCallback(
|
|
18
|
-
(index: number) => {
|
|
19
|
-
const noElementsZeroBased = splittedText.length - 1;
|
|
20
|
-
return {
|
|
21
|
-
in: round(transitionStagger * index, 3),
|
|
22
|
-
out: round(transitionStagger * (noElementsZeroBased - index), 3),
|
|
23
|
-
};
|
|
24
|
-
},
|
|
25
|
-
[splittedText, transitionStagger]
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<span
|
|
30
|
-
className={styles["text"]}
|
|
31
|
-
style={
|
|
32
|
-
{
|
|
33
|
-
"--var-duration": `${transitionDuration}s`,
|
|
34
|
-
} as React.CSSProperties
|
|
35
|
-
}
|
|
36
|
-
>
|
|
37
|
-
{Array.from({ length: 2 }).map((_, i) => (
|
|
38
|
-
<span key={i} className={styles["container"]}>
|
|
39
|
-
{splittedText.map((char, i) => {
|
|
40
|
-
const charDelays = delays(i);
|
|
41
|
-
return (
|
|
42
|
-
<span
|
|
43
|
-
key={i}
|
|
44
|
-
className={styles["char"]}
|
|
45
|
-
style={
|
|
46
|
-
{
|
|
47
|
-
"--var-delay-in": `${charDelays.in}s`,
|
|
48
|
-
"--var-delay-out": `${charDelays.out}s`,
|
|
49
|
-
} as React.CSSProperties
|
|
50
|
-
}
|
|
51
|
-
>
|
|
52
|
-
{char}
|
|
53
|
-
</span>
|
|
54
|
-
);
|
|
55
|
-
})}
|
|
56
|
-
</span>
|
|
57
|
-
))}
|
|
58
|
-
</span>
|
|
59
|
-
);
|
|
60
|
-
}
|
package/src/global.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, RefObject } from "react";
|
|
4
|
-
|
|
5
|
-
type Options = IntersectionObserverInit & {
|
|
6
|
-
once?: boolean;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function useIntersectionObserver(
|
|
10
|
-
ref: RefObject<HTMLElement>,
|
|
11
|
-
options?: Options
|
|
12
|
-
): boolean {
|
|
13
|
-
const [isIntersecting, setIntersecting] = useState(false);
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (!ref.current) return;
|
|
17
|
-
|
|
18
|
-
const observer = new IntersectionObserver(
|
|
19
|
-
([entry]) => {
|
|
20
|
-
if (options?.once) {
|
|
21
|
-
if (entry.isIntersecting) {
|
|
22
|
-
setIntersecting(true);
|
|
23
|
-
observer.disconnect();
|
|
24
|
-
}
|
|
25
|
-
} else {
|
|
26
|
-
setIntersecting(entry.isIntersecting);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
threshold: 0.75,
|
|
31
|
-
...options,
|
|
32
|
-
}
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
observer.observe(ref.current);
|
|
36
|
-
|
|
37
|
-
return () => observer.disconnect();
|
|
38
|
-
}, [ref, options]);
|
|
39
|
-
|
|
40
|
-
return isIntersecting;
|
|
41
|
-
}
|
package/src/utils/numbers.ts
DELETED
package/src/utils/text.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export function sentences(text: string) {
|
|
2
|
-
return segment(text, "sentence");
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function words(text: string) {
|
|
6
|
-
return segment(text, "word");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function characters(text: string) {
|
|
10
|
-
return segment(text, "grapheme");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function segment(
|
|
14
|
-
text: string,
|
|
15
|
-
granularity: "sentence" | "word" | "grapheme"
|
|
16
|
-
) {
|
|
17
|
-
const segmenter = new Intl.Segmenter(undefined, { granularity });
|
|
18
|
-
return Array.from(segmenter.segment(text), (segment) => segment.segment);
|
|
19
|
-
}
|
package/tsconfig.json
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|