重现分片- 相片显示
制作动画的零依赖性复合组件 拆分相片显示就像你在老火车站和机场看到的一样 每个字符会通过完全由CSS驱动的3D旋转来翻转其字符集,而每层(root, slot, phone, flap)都会被曝光,这样你就可以用自己的标记来替换其中的任何一层.
演示
https://hello-mat.com/design-engineering/组件/split-flap-display
特征
- 零运行时间依赖性( Just React)
>= 18(中文(简体) ) - 纯 CSS 三维翻转动画, 硬件加速
- 翻转上个值和下个值之间的每个字符, 就像真实的东西
- 平分字符集(完美地用于时钟,分数板,字母-数字混合显示,.)
- 当值超过可用槽时自动椭圆
- 在每个槽完成翻转时启用可选回调
- 无头API化合物: 默认下注,或作曲
Root编号 :Slot编号 :Character编号 :Flap以插入Tailwind,CSS模块,设计系统原始, - 稳定
data-*选择器和 CSS 自定义属性,用于没有成分的样式 - 装有完整类型脚本的船舶
安装
npm install @daformat/react-split-flap-display
yarn add @daformat/react-split-flap-display
pnpm add @daformat/react-split-flap-display
bun add @daformat/react-split-flap-display
deno add npm:@daformat/react-split-flap-display
快开始
import { useCallback, useRef, useState } from "react";
import { SplitFlapDisplay } from "@daformat/react-split-flap-display";
// see the styling section below
import styles from "./styles.module.css";
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
const WORDS = ["HELLO", "WORLD", "REACT", "FLIP"];
export const Demo = () => {
const [word, setWord] = useState<string>(WORDS[0] ?? "HELLO");
const messageTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const next = useCallback(() => {
if (messageTimeoutRef.current) clearTimeout(messageTimeoutRef.current);
setWord((w) => WORDS[(WORDS.indexOf(w) + 1) % WORDS.length] ?? w);
}, []);
const handleFullyFlipped = useCallback(() => {
if (messageTimeoutRef.current) clearTimeout(messageTimeoutRef.current);
messageTimeoutRef.current = setTimeout(next, 5000);
}, [next]);
return (
<SplitFlapDisplay.Root
value={word}
length={5}
characters={CHARS}
flipDuration={800}
onFullyFlipped={handleFullyFlipped}
className={styles.split_flap_display}
/>
);
};
SplitFlapDisplay.Root 这是你需要99%时间的唯一东西, 它使所有四个嵌入层自动。 这个 Slot, (中文(简体) ), Character 财务报告和审定财务报表 Flap 要自定义内部标记时, 导出已存在; 请参见 组成 节.
说明: 组合
transform-style: preserve-3d内入每层, 但你还需要设置perspective: 550px(或任何价值)Root用于显示3D翻转.
API 概览
软件包导出单个命名空间 SplitFlapDisplay 具有四个复合组件:
<SplitFlapDisplay.Root>
<SplitFlapDisplay.Slot>
<SplitFlapDisplay.Character>
<SplitFlapDisplay.Flap position="top" />
<SplitFlapDisplay.Flap position="bottom" />
</SplitFlapDisplay.Character>
{/* …one Character per character in the set */}
</SplitFlapDisplay.Slot>
{/* …one Slot per `length` */}
</SplitFlapDisplay.Root>
当你不通过一个 children 渲染到某个级别,该级别使该级别自动低于。 所以这四个都是有效的:
// Fully default:
<SplitFlapDisplay.Root value="HI" length={2} characters="ABCDEFGHIJ " />
// Override the slot rendering only:
<SplitFlapDisplay.Root value="HI" length={2} characters="ABCDEFGHIJ ">
{(index, characters, currentCharacter, onFullyFlipped) => (
<SplitFlapDisplay.Slot
key={index}
index={index}
characters={characters}
currentCharacter={currentCharacter}
onFullyFlipped={onFullyFlipped}
className="my-slot"
/>
)}
</SplitFlapDisplay.Root>
// Override the flap rendering for a custom crease overlay, etc.
// (full example in the Composition section below)
SplitFlapDisplay.Root
最外道的包装 拥有值,长度,字符集,和翻转时间. 显示 a <div> 并接受每个标准 <div> 道具 (英语).
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
value | string | —— 说 | 要显示的当前值 。 每个字符必须属于相应的字符集,否则组件会抛出. 带空格的页 value.length < length 并记住包含 " " 内 characters。 。 。 . |
length | number | —— 说 | 要制作的档次数 。 数值短于 length 右加空格; 值长于 length 切换后,最后一个槽变为椭圆(…) (中文(简体) ). |
characters | string | string[] | —— 说 | 每个插槽的字符集可以翻转. 通过一个单一字符串,在每个槽中共享相同的设定,或长度数组 length 以给每个槽自己的设定。 每套必须非空并不含重复. |
onFullyFlipped | () => void | —— 说 | 在每个槽完成翻转到电流后, 精确点火一次 value。在下一个值变化时再次开火。 用于链路转换或同步音频. |
crease | number | string | 1 | 上下相间相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相相 A 类 number 解释为像素; a string 通过逐字记录(例如:. "0.5rem") (中文(简体) ). 被曝光为 CSS --split-flap-crease。 。 。 . |
flipDuration | number | string | 800 | 翻转动画的持续时间 。 A 类 number 解释为毫秒;a string 通过逐字记录(例如:. "1s") (中文(简体) ). 被曝光为 CSS --split-flap-flip-duration。 。 。 . |
flipTimingFunction | string | "cubic-bezier(.215, .61, .355, 1)" | 翻转动画的 CSS 计时功能. 被曝光为 CSS --split-flap-timing-function。 。 。 . |
children | 渲染道具,见下文 | —— 说 | 可选的. 接管插槽渲染。 省略时, Root 制作一个 <SplitFlapDisplay.Slot> 显示值的每个字符. |
style | CSSProperties | —— 说 | 与组件本身的内线样式合并 。 组件的 CSS 变量是最后应用的,并将对通过 style。 。 。 . |
ref | Ref<HTMLDivElement> | —— 说 | 转发到根 <div>。 。 。 . |
...props | Omit<ComponentPropsWithoutRef<"div">, "children"> | —— 说 | 任何其他标准 <div> 道具( r)className, (中文(简体) ), id, (中文(简体) ), aria-*, (中文(简体) ), data-*(中文(简体) ). |
Root children 成文签名
(
index: number, // 0-based slot index
characters: string, // character set for this slot (with the ellipsis appended on the last slot when overflowing)
currentCharacter: string, // the character this slot should currently be showing
onFullyFlipped: (character: string, index: number) => void, // pass this through to your <Slot>
) => ReactNode;
捕获 currentCharacter 从此关闭, 如果您需要更深入地转发它( 它不是由 Slot.children) (中文(简体) ).
SplitFlapDisplay.Slot
显示中的单个插位 : <span data-split-flap-slot=""> 包含槽字符集中每一个可能的字符,一次只有一个是"当前". 转发每个标准 <span> 指向根相间.
| Prop | 类型 | 说明 |
|---|---|---|
index | number | 插槽在显示位置. 用来作为插槽的身份 由 onFullyFlipped 簿记 Root。 。 。 . |
characters | string | 字符设置此槽可以翻转. 必须是非空的,没有重复的,必须包含 currentCharacter。 。 。 . |
currentCharacter | string | 此槽当前应该显示的字符 。 更改此选项会触发翻转动画在两者之间的每一个字符. |
onFullyFlipped | (character: string, index: number) => void | 可选的. 这段时间之后打来的 currentCharacter。当你在下面写作时 Root只要通过 onFullyFlipped 您从 Root做为道具. |
children | (character: string, index: number) => ReactNode | 可选的. 接手角色渲染 在剧集中每个角色调用一次. 省略时, Slot 制作一个 <SplitFlapDisplay.Character> 每个角色. |
style | CSSProperties | 与组件本身的内线样式合并 . |
ref | Ref<HTMLSpanElement> | 转发到槽中 <span>。 。 。 . |
...props | Omit<ComponentPropsWithoutRef<"span">, "children"> | 任何其他标准 <span> 道具 (英语). |
Slot 拥有槽级 CSS 变量(--split-flap-current-character-index, (中文(简体) ), --split-flap-total, (中文(简体) ), --split-flap-turn),也是你最常用一种 className。 。 。 .
SplitFlapDisplay.Character
插槽内可能有一个字符 : <span data-split-flap-character="" data-char="X"> 包含两个旋转相片。 集中的每一个字符都会被渲染出,非当前字符会被定位到当前字符后方/前方的3D空间. 转发每个标准 <span> 道具 (英语).
| Prop | 类型 | 说明 |
|---|---|---|
index | number | 角色在槽的字符集中的位置. |
character | string | 这个字符 Character 代表(一个单一的图表). |
currentCharacter | string | 该插槽目前显示的字符 。 用来计算是否此 Character 是活动者。 活跃人物有: inert 删除属性; 所有其他 inert={true}。 。 。 . |
children | (character: string) => ReactNode | 可选的. 接过襟翼渲染 接收 character,返回两个相片(以及任何额外的相片,如折叠等). 省略时, Character 调制 <Flap position="top"> 接下来 <Flap position="bottom">。 。 。 . |
style | CSSProperties | 与组件本身的内线样式合并 . |
ref | Ref<HTMLSpanElement> | 转发到字符 <span>。 。 。 . |
...props | Omit<ComponentPropsWithoutRef<"span">, "children"> | 任何其他标准 <span> 道具 (英语). |
Character 拥有数学重度每平方英尺 CSS 变量(--split-flap-offset, (中文(简体) ), --split-flap-direction, (中文(简体) ), --split-flap-top-flap-angle, (中文(简体) ), --split-flap-bottom-flap-angle)——见 CSS 自定义属性。 。 。 .
SplitFlapDisplay.Flap
单半相片对: 使一 <span data-split-flap-flap="top|bottom"> 绕上下边缘旋转 转发每个标准 <span> 道具 (英语).
| Prop | 类型 | 说明 |
|---|---|---|
character | string | 这种相扑显示的字符 . |
position | "top" | "bottom" | 这是一半的相片 这个 bottom 襟翼自动 aria-hidden 财务报告和审定财务报表 inert ——是顶部相片的视觉镜. |
style | CSSProperties | 与组件本身的内线样式合并 . |
ref | Ref<HTMLSpanElement> | 传到襟翼 <span>。 。 。 . |
...props | ComponentPropsWithoutRef<"span"> | 任何其他标准 <span> 道具( r)className(见a/c.5/49/70,附件). |
组成(尾风实例)
渲染道具的插槽允许您将任何一层换成自己的标记. 共同原因:
- 尾风/ 公用事业类样式 —— 说
className打开Slot编号 :Character编号 :Flap工作时没有任何后代选择者. - 添加额外元素 -例如,一个真实的
<span>折叠而不是::after伪元素 (Tailwind不以伪元素作好). - 完全跳过默认平板标记 ——将每个角色都包裹在自己设计-系统原始中.
这里的机场板看起来像 样式 例如,完全用 Tailwind 工具类和组成来写。 说明 <span aria-hidden> 在两个相片之间替换 ::after 口罩。 。 .
import { useCallback, useRef, useState } from "react";
import { SplitFlapDisplay } from "@daformat/react-split-flap-display";
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
const WORDS = ["HELLO", "WORLD", "REACT", "FLIP"];
const FLAP =
"bg-[#feefe7] box-content h-[0.5em] w-[1em] leading-none rounded-[3px] " +
"shadow-[inset_0_0_0_1px_rgba(255,255,255,0.6)]";
export const Demo = () => {
const [word, setWord] = useState<string>(WORDS[0] ?? "HELLO");
const messageTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const next = useCallback(() => {
if (messageTimeoutRef.current) clearTimeout(messageTimeoutRef.current);
setWord((w) => WORDS[(WORDS.indexOf(w) + 1) % WORDS.length] ?? w);
}, []);
const handleFullyFlipped = useCallback(() => {
if (messageTimeoutRef.current) clearTimeout(messageTimeoutRef.current);
messageTimeoutRef.current = setTimeout(next, 5000);
}, [next]);
return (
<SplitFlapDisplay.Root
value={word}
length={5}
characters={CHARS}
flipDuration={800}
onFullyFlipped={handleFullyFlipped}
className="flex gap-[2px] text-[3.5em] [filter:drop-shadow(0_1px_12px_rgba(102,27,33,0.05))]"
>
{(index, characters, currentCharacter, onFullyFlipped) => (
<SplitFlapDisplay.Slot
key={index}
index={index}
characters={characters}
currentCharacter={currentCharacter}
onFullyFlipped={onFullyFlipped}
>
{(character, characterIndex) => (
<SplitFlapDisplay.Character
key={character}
index={characterIndex}
character={character}
currentCharacter={currentCharacter}
>
{(c) => (
<>
<SplitFlapDisplay.Flap
character={c}
position="top"
className={`${FLAP} items-start pt-[0.25em]`}
/>
{/*
A real <span> instead of an ::after pseudo-element so
Tailwind users don't need arbitrary after:* variants.
Masks the gap between the two flaps so nothing shows
through the crease during the flip.
*/}
<span
aria-hidden
className="absolute inset-x-0 top-1/2 -translate-y-1/2 bg-[#feefe7]"
style={{ height: "var(--split-flap-crease)" }}
/>
<SplitFlapDisplay.Flap
character={c}
position="bottom"
className={`${FLAP} items-end pb-[0.25em]`}
/>
</>
)}
</SplitFlapDisplay.Character>
)}
</SplitFlapDisplay.Slot>
)}
</SplitFlapDisplay.Root>
);
};
几事须知:
currentCharacter实值 没有 由Slot.children或者说Character.children但是,它不一定是 - 你有范围 从Root把它直接传下来.onFullyFlipped从Root'渲染道具是每片地的记者. 直接传到Slot作为自己的onFullyFlipped道具 (英语).Root已取消每个槽索引并点出自己的onFullyFlipped每个值变化一次.- 你自由只写你关心的级别 如果您只需要样式
Slot有了班级,你不需要制造Character或者说Flap- 默认渲染处理它们.
渲染出的结构和数据属性( D)
这个部件使DOM树相当小. 数据属性是稳定的选择器,您可以用来从样式表瞄准单个部分.
<div> <!-- Root -->
<span data-split-flap-slot=""> <!-- Slot, one per `length` -->
<span data-split-flap-character="" <!-- Character, one per character in the set -->
data-char="A">
<span data-split-flap-flap="top">A</span> <!-- Flap position="top" -->
<span data-split-flap-flap="bottom">A</span> <!-- Flap position="bottom" -->
</span>
<!-- … one Character span per character in the set -->
</span>
<!-- … one Slot per `length` -->
</div>
| 属性 | 何处 | 说明 |
|---|---|---|
data-split-flap-slot="" | 每个 Slot | 标记其中之一 length 构成显示的插槽 . |
data-split-flap-character="" | 每个 Character | 在槽中标记一个可能的字符。 目前可见的字符是索引匹配的字符 --split-flap-current-character-index 在它的母档. |
data-char="X" | 每个 Character | 这个字符 Character 代表着. |
data-split-flap-flap="top" | 顶 Flap | 旋转的一半 0deg 降为 -90deg 一边翻来翻去. |
data-split-flap-flap="bottom" | 底层 Flap | 旋转的一半 90deg 最多 0deg 一边翻来翻去 永远 inert 财务报告和审定财务报表 aria-hidden。 。 。 . |
CSS 自定义属性
该组件通过 CSS 自定义属性来暴露其动画状态,这样您就可以在不触及组件内部的情况下,从自己的样式表来样式和主题化相片.
开始 Root (单位:千美元)<div>(中文(简体) )
| 财产 | 设定从 |
|---|---|
--split-flap-crease | 这个 crease 道具 (英语). 上下相扑的明显间隙. |
--split-flap-flip-duration | 这个 flipDuration 道具 (英语). 翻转动画持续时间. |
--split-flap-timing-function | 这个 flipTimingFunction 道具 (英语). 时间函数应用于翻转动画. |
各设定 Slot (单位:千美元)[data-split-flap-slot](中文(简体) )
| 财产 | 说明 |
|---|---|
--split-flap-current-character-index | 插槽字符集中当前可见字符的索引. 在槽动画通过中间字符时不断更新. |
--split-flap-total | 插槽字符集中的字符总数. |
--split-flap-turn | 内部旋转计数器。 每两个回合重置一次,以避免萨法里精密闪烁并避免长跑的显示会出现整数外溢. |
各设定 Character (单位:千美元)[data-split-flap-character](中文(简体) )
这些大多是内部的——你一般不需要读取或覆盖它们,但是它们被记录下来了,因为它们在dev工具中是计算和可见的.
| 财产 | 说明 |
|---|---|
--split-flap-index | 插槽字符集内的人物索引. |
--split-flap-offset | 与当前字符的已签名距离( N)index − current) (中文(简体) ). |
--split-flap-direction | 1 如果这个字符领先于当前字符, -1 如果落后的话, 0 如果这是现在的. |
--split-flap-is-current | 1 对于明显的字符, 0 否则。 用于有选择地为当前相片配对提供样式. |
--split-flap-is-previous | 1 对于当前字符之前的字符, 0 否则. |
--split-flap-is-next | 1 在当前字符之后, 0 否则. |
--split-flap-top-flap-angle | 这个 rotateX 角度. |
--split-flap-bottom-flap-angle | 这个 rotateX 角度. |
样式
显示的两种样式:
- 默认渲染 +
data-*选择器 ——以香草CSS/CSS模块/SCSS最容易. 放下课Root并用属性瞄准内部的片段。 下面的例子就是这样. - 组成 - 通过
className直接转到Slot, (中文(简体) ),Character, (中文(简体) ),Flap从Root做为道具 容易使用Tailwind等功能级框架,如果需要添加额外的DOM(如真元素折叠),则需要使用.
说明: 组件已设置
transform-style: preserve-3d打开Root, (中文(简体) ),Slot财务报告和审定财务报表Character但你还是需要设置perspective的父要素Root用于3D翻转实际可见.
.split_flap_display {
display: flex;
font-size: 3.5em;
gap: 2px;
filter: drop-shadow(0 1px 12px rgba(102, 27, 33, 0.05));
[data-split-flap-character] {
/* prevent things from showing through the crease during the flip */
&::after {
background-color: #feefe7;
content: "";
display: block;
/* this variable is set by the component */
height: var(--split-flap-crease);
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
}
> [data-split-flap-flap] {
background: #feefe7;
border-radius: 3px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
box-sizing: content-box;
height: 0.5em;
line-height: 1;
width: 1em;
&[data-split-flap-flap="top"] {
align-items: flex-start;
padding-top: 0.25em;
}
&[data-split-flap-flap="bottom"] {
align-items: flex-end;
padding-bottom: 0.25em;
}
}
}
}
实例
每集字符集( 点)
通过一系列字符串来给每个槽自己的字符集. 这比在任何地方使用一个大集要有效得多,因为每个插槽只需要翻转它实际能够显示的字符.
const time = new Date();
const value =
String(time.getHours()).padStart(2, "0") +
":" +
String(time.getMinutes()).padStart(2, "0");
<SplitFlapDisplay.Root
value={value}
length={5}
characters={["012", "0123456789", ":", "012345", "0123456789"]}
/>;
与椭圆相重叠
何时 value.length > length,该值被切换并用椭圆替换上一个插槽(…) (中文(简体) ). 椭圆会自动被添加到上一个槽的字符集中,所以它在那里是一个有效的字符.
<SplitFlapDisplay.Root
value="DEPARTURES"
length={6}
characters="ABCDEFGHIJKLMNOPQRSTUVWXYZ "
/>
// renders: D E P A R …
翻转完成后反应
<SplitFlapDisplay.Root
value={value}
length={8}
characters={CHARS}
onFullyFlipped={() => {
// Every slot has settled on its final character.
playClickSound();
}}
/>
onFullyFlipped 每段动画完成后,每个值都会发生一次变化 value ——包括由于性格已经正确而未移动的插槽.
浏览器支持
在所有的常绿浏览器工作。 该部分包含一些用于Safari的小作品(a) translateZ(0.1px) 修复一个 backface-visibility 在动画中出现故障, 并重置一个回合计数器来躲避特定旋转值.
类型脚本
类型是捆绑的。 该包再出口每个化合物组件的道具类型:
import {
SplitFlapDisplay,
type SplitFlapDisplayRootProps,
type SplitFlapDisplaySlotProps,
type SplitFlapDisplayCharacterProps,
type SplitFlapDisplayFlapProps,
} from "@daformat/react-split-flap-display";
每个道具类型扩展 Omit<ComponentPropsWithoutRef<...>, "children"> 加上组件特定道具,可以直接得出包装类型:
import {
SplitFlapDisplay,
type SplitFlapDisplayRootProps,
} from "@daformat/react-split-flap-display";
type ScoreBoardProps = Omit<SplitFlapDisplayRootProps, "characters" | "length">;
const ScoreBoard = (props: ScoreBoardProps) => (
<SplitFlapDisplay.Root {...props} length={4} characters="0123456789 " />
);
许可证
零相机 BSD - 随你怎么说.