cat-paw-cup-react
v0.2.4
Published
Draggable cat paw cup with chili drops and classic overflow, extracted from the original blog experience.
Readme
cat-paw-cup-react
可拖动的猫爪杯 React 组件,提供礼物掉落、物理碰撞和爆杯动画。支持全屏悬浮、容器内展示、自定义礼物图片,以及通过参数和 ref 控制交互。
安装
在已有 React 项目中安装组件和 Motion:
npm install cat-paw-cup-react motion@^12.38.0环境要求:
- React 和 React DOM:18 或 19,使用相同主版本。
- Motion:12.x,最低 12.38.0。
- Node.js:20.19.0 或更高。
- 模块格式:ESM,支持 TypeScript 类型提示。
组件样式需要单独导入:
import 'cat-paw-cup-react/style.css'默认杯子和礼物图片已包含在包中,无需复制素材。使用 CSP 的网站需在 img-src 中允许 data:;使用远程自定义图片时,还需允许对应图片域名。
快速开始
import { CatPawCupScene } from 'cat-paw-cup-react'
import 'cat-paw-cup-react/style.css'
export default function App() {
return <CatPawCupScene />
}默认行为:
- 在整个视口中显示一个可拖动的杯子。
- 素材准备完成后等待 1000ms,再淡入 450ms。
- 自动掉落 100 个内置礼物;后续每次默认掉落也是 100 个。
- 每轮累计生成 300 个礼物时触发爆杯,再开始下一轮计数。
- 默认水平居中并贴齐场景底部;窄屏下可能因右下角预留区而上移。
点击按钮触发掉落
heartSignal 是递增的触发信号。每增加 1,就请求一次 dropQuantity 数量的掉落。
import { useState } from 'react'
import { CatPawCupScene } from 'cat-paw-cup-react'
import 'cat-paw-cup-react/style.css'
export default function InteractiveCup() {
const [signal, setSignal] = useState(0)
return (
<>
<button
type="button"
style={{ position: 'fixed', right: 24, bottom: 40, zIndex: 90 }}
onClick={() => setSignal(value => value + 1)}
>
添加礼物
</button>
<CatPawCupScene
heartSignal={signal}
autoDrop={false}
dropQuantity={20}
/>
</>
)
}信号应从 0 开始,不要传入数据库中的历史累计数量。信号减少时只重设基准,不会移除礼物。也可以不传 heartSignal,改用下方的 ref 方法。
容器内展示与主动控制
父容器必须有明确的宽度和高度。下面的示例可以直接使用内置素材:
import { useRef } from 'react'
import { CatPawCupScene, type CatPawCupSceneHandle } from 'cat-paw-cup-react'
import 'cat-paw-cup-react/style.css'
export default function GiftBox() {
const cup = useRef<CatPawCupSceneHandle>(null)
return (
<>
<button type="button" onClick={() => { void cup.current?.drop().catch(console.error) }}>
添加一批
</button>
<button type="button" onClick={() => { void cup.current?.drop(7).catch(console.error) }}>
添加 7 个
</button>
<button type="button" onClick={() => cup.current?.clear()}>清空</button>
<button type="button" onClick={() => cup.current?.reset()}>重置</button>
<button type="button" onClick={() => { void cup.current?.overflow().catch(console.error) }}>
爆杯
</button>
<div style={{ width: '100%', height: 560 }}>
<CatPawCupScene
ref={cup}
mode="container"
autoDrop={false}
dropQuantity={20}
overflowThreshold={150}
cupScale={0.8}
onError={error => console.error(error)}
/>
</div>
</>
)
}容器模式会跟随父容器尺寸变化,并裁剪超出容器的图形。容器只有底部物理边界,顶部和两侧没有物理墙壁。可以同时使用多个独立容器;全屏模式通常只挂载一个实例。
位置、缩放与底部对齐
initialPosition 表示杯子左上角,单位为场景内的 CSS 像素:全屏模式以视口左上角为原点,容器模式以容器左上角为原点。
<CatPawCupScene
mode="container"
cupScale={0.8}
initialPosition={{ x: 80, y: 120 }}
/>杯子设计尺寸为 305 × 323。cupScale 是期望缩放上限;可用空间不足时,组件会继续缩小杯子。
未设置 initialPosition 时,杯子水平居中、底部贴齐场景。向下拖动可以到达底部,reset() 会恢复当前配置的初始位置。显式传入的位置会被限制在可拖动范围内。
全屏模式在视口宽度小于 640px 时,默认避让右下角宽 104px、高 120px 的区域。杯子与该区域重叠时会向上移,并与该区域保留 8px 距离。需要关闭避让时:
<CatPawCupScene reserveHeartArea={false} />关闭后应由使用方安排页面按钮的位置,避免与杯子重叠。容器模式不使用这个预留区。
自定义礼物
<CatPawCupScene
gift={{
image: '/images/star.png',
size: 24,
collisionRadius: 12,
restitution: 0.3,
}}
/>请将图片路径替换为项目中可访问的资源。图片保持纵横比,size 指最长边;collisionRadius 指礼物之间的圆形碰撞半径,杯壁碰撞另按图片旋转包络处理。透明留白较大的图片建议先裁掉多余留白。
自定义图片加载失败、超时或被跨域策略拒绝时,组件会调用 onError,并回退到内置礼物图片及其默认物理配置。
CupGift 参数
| 属性 | 默认值 | 含义 |
| --- | --- | --- |
| image | 内置辣条 | 相对路径、HTTP(S) 或 data URL |
| size | 18.3 | scale=1 时图片最长边(CSS px) |
| collisionRadius | size / 2 | scale=1 时礼物间碰撞半径 |
| mass | 原辣条质量 | 质量,实际按 scale² 缩放 |
| friction | 原辣条配置 | 摩擦系数,0–1 |
| airFriction | 原辣条配置 | 空气阻力,0–1 |
| restitution | 原辣条配置 | 弹性,0–1 |
组件参数
所有参数均可选。下表中的默认值适用于未传入对应参数的情况。
| 参数 | 类型 / 默认值 | 含义 |
| --- | --- | --- |
| heartSignal | number / 0 | 单调递增的点击信号;减少时只重设基准 |
| autoDrop | boolean / true | 挂载或 reset 后是否自动掉落 |
| initialQuantity | number / 100 | 自动掉落数量,可为 0 |
| dropQuantity | number / 100 | 每次点击、无参数 drop() 的数量,可为 0 |
| overflowThreshold | number / 300 | 每轮累计生成数量的爆杯阈值,包含自动掉落和杯外礼物 |
| cupScale | number / 1 | 设计尺寸 305 × 323 的缩放上限,会继续缩小以适配场景 |
| initialPosition | { x, y } / 水平居中,底部贴齐场景 | 杯子左上角,场景内 CSS 像素,自动限制到可拖范围;移动端预留区可能进一步上移 |
| spawnPosition | 'cup' \| { x, y } / 'cup' | 礼物出生点;坐标是场景内 CSS 像素,可在杯外 |
| spawnOffset | { x, y } / { x: 0, y: -200 } | 相对杯口中心的设计像素偏移,乘杯子实际 scale;仅 cup 模式生效 |
| gift | CupGift / 辣条 | 见下表 |
| draggable | boolean / true | 是否允许拖动 |
| paused | boolean / false | 暂停物理、掉落和爆杯;新命令保留在队列中 |
| mode | 'viewport' \| 'container' / 'viewport' | 全屏悬浮或填满当前父容器 |
| reserveHeartArea | boolean / true | 全屏且宽度小于 640px 时,给右下角爱心留空;容器模式不留空 |
| entryDelayMs | number / 1000 | 素材准备好后的入场等待;随后淡入 450ms |
| dropGapMs | number / 10 | 同批两个礼物生成间隔,毫秒 |
| gravity | number / 1 | 重力系数,0 表示无重力 |
| zIndex | number / 80 | Canvas 层级;拖动层 +1,提示层 +2 |
| className | string | 容器根节点 / 全屏 Canvas 层的附加类 |
| style | CSSProperties | 容器根节点样式,仅 container 模式使用 |
| onReady | () => void | 场景初始化完成,可调用 ref;无需等待它才能排队 |
| onError | (error: Error) => void | 资源失败等错误;自定义图片失败是可恢复错误 |
| onOverflowStart | () => void | 每次自动 / 手动爆杯开始 |
| onOverflowEnd | () => void | 爆杯与杯子恢复均完成;取消时不触发 |
数值范围
- 数量取整并限制在 0–10000;爆杯阈值至少为 1。
cupScale限制在 0.05–5;实际尺寸仍受场景空间限制。- 礼物尺寸限制在 1–512,重力限制在 0–10。
- 非法数值回退到默认值。
- 固定出生点可以在场景外,不会自动限制到可见区域。
spawnPosition="cup"在每批开始时确定出生点,不会在该批掉落期间持续追踪杯子。
Ref 方法与执行顺序
| 方法 | 返回值 | 行为 |
| --- | --- | --- |
| drop(quantity?) | Promise<void> | 将掉落请求加入队列;省略数量时使用 dropQuantity。 |
| overflow() | Promise<void> | 排在已有请求之后,执行一次爆杯并重置本轮计数。 |
| clear() | void | 取消当前与排队中的请求,清除礼物和特效,计数归零;保持杯子位置,不自动掉落。 |
| reset() | void | 清空场景并恢复当前 initialPosition,根据 autoDrop 和 initialQuantity 决定是否重新自动掉落。 |
掉落请求按顺序处理,达到阈值时拆分批次。例如,阈值为 10、请求 25 个礼物,会执行:
掉落 10 个 → 爆杯 → 掉落 10 个 → 爆杯 → 掉落 5 个drop() 的 Promise 在整个请求及其触发的爆杯完成后兑现,不表示所有礼物已经落稳。clear()、reset() 或卸载取消请求时,相关 Promise 正常兑现;执行错误会 reject,并调用 onError。调用返回 Promise 的方法时应处理错误。
参数更新规则
| 修改内容 | 生效方式 |
| --- | --- |
| 缩放、重力、掉落间隔、暂停、拖动开关 | 更新后立即生效。 |
| 礼物图片与物理配置 | 下一批生效;已有礼物保留其图片。 |
| dropQuantity | 每次请求入队时确定数量。 |
| overflowThreshold | 在下一个批次分段边界读取。 |
| autoDrop、initialQuantity、initialPosition | 不会自动重放;调用 reset() 后按新配置重新开始。 |
| mode | 重建场景。 |
仅重新创建回调函数或内容相同的配置对象,不会重建整个物理场景。
暂停与减少动态效果
paused={true} 或标签页隐藏时,物理模拟、掉落和爆杯暂停;恢复后继续执行,不补跑暂停期间的时间。暂停期间的新命令保留在队列中。
系统开启“减少动态效果”时,组件先显示“启用互动”按钮。用户启用后才执行等待中的 ref 命令;启用前的 heartSignal 信号会被忽略。autoDrop={false} 只控制自动掉落,不会关闭这项无障碍行为。
Next.js App Router
在客户端组件中使用 next/dynamic 延迟加载,并关闭该组件的服务端渲染:
'use client'
import dynamic from 'next/dynamic'
import 'cat-paw-cup-react/style.css'
const CatPawCupScene = dynamic(() => import('cat-paw-cup-react'), { ssr: false })
export default function Cup() {
return <CatPawCupScene />
}在需要显示组件的页面或布局中引用 Cup。如果要随页面切换重新初始化,可改变组件的 key;如果要保留场景,可将其挂载在共享布局中。
常见问题
组件没有样式或拖动区域不正确? 确认已导入 cat-paw-cup-react/style.css。
容器模式没有显示? 确认父容器有明确宽高,并检查资源错误回调。
窄屏下杯子没有贴底? 检查 reserveHeartArea;默认会避让右下角区域。显式设置的 initialPosition 也会影响初始位置。
改了初始位置或首次掉落数量,为什么没有重播? 这些参数不会触发自动重置;通过 ref 调用 reset()。
没有传入交互信号也会掉落? autoDrop 默认为 true;需要完全由按钮或 ref 控制时,将其设为 false。
