@chao-component/bag-animation-ui 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/dist/assets/bag/blind_box_open.png +0 -0
- package/dist/assets/bag/blind_box_spin.png +0 -0
- package/dist/assets/bag/blind_box_tear_off.png +0 -0
- package/dist/assets/bag/blind_box_unopen.png +0 -0
- package/dist/assets/bag.png +0 -0
- package/dist/index.d.mts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +343 -0
- package/dist/index.mjs +332 -0
- package/package.json +9 -4
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,32 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
interface BagAnimationProps {
|
|
4
|
+
isAnimating: boolean;
|
|
5
|
+
setIsAnimating: (isAnimating: boolean) => void;
|
|
6
|
+
doneFunction: () => void;
|
|
7
|
+
frames?: string[];
|
|
8
|
+
defaultImage?: string;
|
|
9
|
+
defaultImageAlt?: string;
|
|
10
|
+
swipeHintText?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function BagAnimation({ isAnimating, setIsAnimating, doneFunction, frames, defaultImage, defaultImageAlt, swipeHintText }: BagAnimationProps): react_jsx_runtime.JSX.Element;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取打包后的资源路径
|
|
16
|
+
* 这些资源会被打包到 dist/assets 目录中
|
|
17
|
+
*
|
|
18
|
+
* 使用方式:
|
|
19
|
+
* 1. 如果使用默认资源,可以通过 getAssetPath 获取路径
|
|
20
|
+
* 2. 或者直接传入自定义路径覆盖默认值
|
|
21
|
+
*/
|
|
22
|
+
declare function getAssetPath(relativePath: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* 默认的动画帧路径
|
|
25
|
+
*/
|
|
26
|
+
declare const defaultAnimationFrames: string[];
|
|
27
|
+
/**
|
|
28
|
+
* 默认的图片路径
|
|
29
|
+
*/
|
|
30
|
+
declare const defaultImagePath = "/assets/bag.png";
|
|
31
|
+
|
|
32
|
+
export { BagAnimation, type BagAnimationProps, defaultAnimationFrames, defaultImagePath, getAssetPath };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,32 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
interface BagAnimationProps {
|
|
4
|
+
isAnimating: boolean;
|
|
5
|
+
setIsAnimating: (isAnimating: boolean) => void;
|
|
6
|
+
doneFunction: () => void;
|
|
7
|
+
frames?: string[];
|
|
8
|
+
defaultImage?: string;
|
|
9
|
+
defaultImageAlt?: string;
|
|
10
|
+
swipeHintText?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function BagAnimation({ isAnimating, setIsAnimating, doneFunction, frames, defaultImage, defaultImageAlt, swipeHintText }: BagAnimationProps): react_jsx_runtime.JSX.Element;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 获取打包后的资源路径
|
|
16
|
+
* 这些资源会被打包到 dist/assets 目录中
|
|
17
|
+
*
|
|
18
|
+
* 使用方式:
|
|
19
|
+
* 1. 如果使用默认资源,可以通过 getAssetPath 获取路径
|
|
20
|
+
* 2. 或者直接传入自定义路径覆盖默认值
|
|
21
|
+
*/
|
|
22
|
+
declare function getAssetPath(relativePath: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* 默认的动画帧路径
|
|
25
|
+
*/
|
|
26
|
+
declare const defaultAnimationFrames: string[];
|
|
27
|
+
/**
|
|
28
|
+
* 默认的图片路径
|
|
29
|
+
*/
|
|
30
|
+
declare const defaultImagePath = "/assets/bag.png";
|
|
31
|
+
|
|
32
|
+
export { BagAnimation, type BagAnimationProps, defaultAnimationFrames, defaultImagePath, getAssetPath };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
8
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
9
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
10
|
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
11
15
|
var __copyProps = (to, from, except, desc) => {
|
|
12
16
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
17
|
for (let key of __getOwnPropNames(from))
|
|
@@ -770,9 +774,348 @@ var require_lib = __commonJS({
|
|
|
770
774
|
|
|
771
775
|
// src/index.ts
|
|
772
776
|
var index_exports = {};
|
|
777
|
+
__export(index_exports, {
|
|
778
|
+
BagAnimation: () => BagAnimation,
|
|
779
|
+
defaultAnimationFrames: () => defaultAnimationFrames,
|
|
780
|
+
defaultImagePath: () => defaultImagePath,
|
|
781
|
+
getAssetPath: () => getAssetPath
|
|
782
|
+
});
|
|
773
783
|
module.exports = __toCommonJS(index_exports);
|
|
774
784
|
|
|
775
785
|
// src/BagAnimation.tsx
|
|
776
786
|
var import_react = require("react");
|
|
777
787
|
var import_apng_js = __toESM(require_lib());
|
|
788
|
+
|
|
789
|
+
// src/assets.ts
|
|
790
|
+
function getAssetPath(relativePath) {
|
|
791
|
+
const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
|
|
792
|
+
return `/assets/${cleanPath}`;
|
|
793
|
+
}
|
|
794
|
+
var defaultAnimationFrames = [
|
|
795
|
+
"/assets/bag/blind_box_spin.png",
|
|
796
|
+
"/assets/bag/blind_box_unopen.png",
|
|
797
|
+
"/assets/bag/blind_box_tear_off.png",
|
|
798
|
+
"/assets/bag/blind_box_open.png"
|
|
799
|
+
];
|
|
800
|
+
var defaultImagePath = "/assets/bag.png";
|
|
801
|
+
|
|
802
|
+
// src/BagAnimation.tsx
|
|
778
803
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
804
|
+
function BagAnimation({
|
|
805
|
+
isAnimating,
|
|
806
|
+
setIsAnimating,
|
|
807
|
+
doneFunction,
|
|
808
|
+
frames = defaultAnimationFrames,
|
|
809
|
+
defaultImage = defaultImagePath,
|
|
810
|
+
defaultImageAlt = "Weedza Mystery Box",
|
|
811
|
+
swipeHintText = "Swipe to open"
|
|
812
|
+
}) {
|
|
813
|
+
const [frame, setFrame] = (0, import_react.useState)(0);
|
|
814
|
+
const [isPausedAtThird, setIsPausedAtThird] = (0, import_react.useState)(false);
|
|
815
|
+
const [thirdFrameProgress, setThirdFrameProgress] = (0, import_react.useState)(0);
|
|
816
|
+
const [showSwipeHint, setShowSwipeHint] = (0, import_react.useState)(true);
|
|
817
|
+
const canvasRef = (0, import_react.useRef)(null);
|
|
818
|
+
const apngRefs = (0, import_react.useRef)([]);
|
|
819
|
+
const animationRefs = (0, import_react.useRef)([]);
|
|
820
|
+
const thirdFramePlayerRef = (0, import_react.useRef)(null);
|
|
821
|
+
const thirdFrameCtxRef = (0, import_react.useRef)(null);
|
|
822
|
+
const thirdFrameApngRef = (0, import_react.useRef)(null);
|
|
823
|
+
const thirdFrameOriginalSizeRef = (0, import_react.useRef)(null);
|
|
824
|
+
const hasTriggeredFourthFrame = (0, import_react.useRef)(false);
|
|
825
|
+
const mysteryBoxImage = defaultImage;
|
|
826
|
+
const mysteryBoxAlt = defaultImageAlt;
|
|
827
|
+
(0, import_react.useEffect)(() => {
|
|
828
|
+
if (isAnimating) {
|
|
829
|
+
setFrame(0);
|
|
830
|
+
setIsPausedAtThird(false);
|
|
831
|
+
setThirdFrameProgress(0);
|
|
832
|
+
setShowSwipeHint(true);
|
|
833
|
+
hasTriggeredFourthFrame.current = false;
|
|
834
|
+
} else {
|
|
835
|
+
animationRefs.current.forEach((anim) => {
|
|
836
|
+
if (anim) anim.stop();
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}, [isAnimating]);
|
|
840
|
+
(0, import_react.useEffect)(() => {
|
|
841
|
+
if (!isAnimating || !canvasRef.current) return;
|
|
842
|
+
const canvas = canvasRef.current;
|
|
843
|
+
const ctx = canvas.getContext("2d");
|
|
844
|
+
if (!ctx) return;
|
|
845
|
+
if (animationRefs.current[frame]) {
|
|
846
|
+
animationRefs.current[frame].stop();
|
|
847
|
+
}
|
|
848
|
+
const loadAPNG = async () => {
|
|
849
|
+
try {
|
|
850
|
+
const response = await fetch(frames[frame]);
|
|
851
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
852
|
+
const apng = await (0, import_apng_js.default)(arrayBuffer);
|
|
853
|
+
if (apng instanceof Error) {
|
|
854
|
+
console.error("Failed to parse APNG:", apng);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
canvas.width = apng.width;
|
|
858
|
+
canvas.height = apng.height;
|
|
859
|
+
apngRefs.current[frame] = apng;
|
|
860
|
+
await apng.createImages();
|
|
861
|
+
const player = await apng.getPlayer(ctx);
|
|
862
|
+
let isStopped = false;
|
|
863
|
+
let stopFunction = null;
|
|
864
|
+
let totalDuration = 0;
|
|
865
|
+
for (let i = 0; i < apng.frames.length; i++) {
|
|
866
|
+
const frameDelay = apng.frames[i].delay || 100;
|
|
867
|
+
totalDuration += frameDelay / 1e3 * 1e3;
|
|
868
|
+
}
|
|
869
|
+
player.play(1);
|
|
870
|
+
if (player.stop) {
|
|
871
|
+
stopFunction = player.stop.bind(player);
|
|
872
|
+
}
|
|
873
|
+
animationRefs.current[frame] = {
|
|
874
|
+
stop: () => {
|
|
875
|
+
isStopped = true;
|
|
876
|
+
if (stopFunction) {
|
|
877
|
+
stopFunction();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
if (frame === 2) {
|
|
882
|
+
if (stopFunction) {
|
|
883
|
+
stopFunction();
|
|
884
|
+
}
|
|
885
|
+
thirdFramePlayerRef.current = player;
|
|
886
|
+
thirdFrameCtxRef.current = ctx;
|
|
887
|
+
thirdFrameApngRef.current = apng;
|
|
888
|
+
thirdFrameOriginalSizeRef.current = {
|
|
889
|
+
width: apng.width,
|
|
890
|
+
height: apng.height
|
|
891
|
+
};
|
|
892
|
+
setTimeout(() => {
|
|
893
|
+
setIsPausedAtThird(true);
|
|
894
|
+
setThirdFrameProgress(0);
|
|
895
|
+
}, 0);
|
|
896
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
897
|
+
animationRefs.current[frame].stop = () => {
|
|
898
|
+
isStopped = true;
|
|
899
|
+
if (stopFunction) {
|
|
900
|
+
stopFunction();
|
|
901
|
+
}
|
|
902
|
+
originalStop();
|
|
903
|
+
};
|
|
904
|
+
} else {
|
|
905
|
+
const timeoutId = setTimeout(() => {
|
|
906
|
+
if (!isStopped) {
|
|
907
|
+
if (frame < frames.length - 1) {
|
|
908
|
+
setFrame(frame + 1);
|
|
909
|
+
} else {
|
|
910
|
+
setIsAnimating(false);
|
|
911
|
+
doneFunction();
|
|
912
|
+
setFrame(0);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}, totalDuration);
|
|
916
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
917
|
+
animationRefs.current[frame].stop = () => {
|
|
918
|
+
isStopped = true;
|
|
919
|
+
clearTimeout(timeoutId);
|
|
920
|
+
if (stopFunction) {
|
|
921
|
+
stopFunction();
|
|
922
|
+
}
|
|
923
|
+
originalStop();
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
console.error("Error loading APNG:", error);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
loadAPNG();
|
|
931
|
+
return () => {
|
|
932
|
+
if (animationRefs.current[frame]) {
|
|
933
|
+
animationRefs.current[frame].stop();
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}, [frame, isAnimating, setIsAnimating, doneFunction]);
|
|
937
|
+
(0, import_react.useEffect)(() => {
|
|
938
|
+
if (isPausedAtThird && thirdFrameCtxRef.current && thirdFrameApngRef.current && thirdFramePlayerRef.current && canvasRef.current && thirdFrameOriginalSizeRef.current) {
|
|
939
|
+
const apng = thirdFrameApngRef.current;
|
|
940
|
+
const ctx = thirdFrameCtxRef.current;
|
|
941
|
+
const canvas = canvasRef.current;
|
|
942
|
+
const player = thirdFramePlayerRef.current;
|
|
943
|
+
const originalSize = thirdFrameOriginalSizeRef.current;
|
|
944
|
+
canvas.width = originalSize.width;
|
|
945
|
+
canvas.height = originalSize.height;
|
|
946
|
+
const totalFrames = apng.frames.length;
|
|
947
|
+
const targetFrameIndex = Math.floor(thirdFrameProgress / 100 * totalFrames);
|
|
948
|
+
const clampedFrameIndex = Math.min(Math.max(0, targetFrameIndex), totalFrames - 1);
|
|
949
|
+
if (apng.frames[clampedFrameIndex]) {
|
|
950
|
+
try {
|
|
951
|
+
ctx.save();
|
|
952
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
953
|
+
ctx.clearRect(0, 0, originalSize.width, originalSize.height);
|
|
954
|
+
if (player.render && typeof player.render === "function") {
|
|
955
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
956
|
+
canvas.width = originalSize.width;
|
|
957
|
+
canvas.height = originalSize.height;
|
|
958
|
+
}
|
|
959
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
960
|
+
} else {
|
|
961
|
+
const frame2 = apng.frames[clampedFrameIndex];
|
|
962
|
+
if (frame2.imageElement && frame2.imageElement instanceof HTMLImageElement) {
|
|
963
|
+
const img = frame2.imageElement;
|
|
964
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
965
|
+
} else if (frame2.image && frame2.image instanceof HTMLImageElement) {
|
|
966
|
+
const img = frame2.image;
|
|
967
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
968
|
+
} else {
|
|
969
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
970
|
+
canvas.width = originalSize.width;
|
|
971
|
+
canvas.height = originalSize.height;
|
|
972
|
+
}
|
|
973
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
ctx.restore();
|
|
977
|
+
} catch (error) {
|
|
978
|
+
console.error("Error rendering frame:", error);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}, [thirdFrameProgress, isPausedAtThird]);
|
|
983
|
+
const ctxDrawSwipeFrames = (ctx, img, clampedFrameIndex, originalSize) => {
|
|
984
|
+
let buttonGapPercentage = 0;
|
|
985
|
+
let startX = originalSize.width * 0.30685;
|
|
986
|
+
let startY = originalSize.height - (originalSize.height * buttonGapPercentage + img.height);
|
|
987
|
+
if (clampedFrameIndex == 0) {
|
|
988
|
+
ctx.drawImage(img, 0, 0, originalSize.width, originalSize.height);
|
|
989
|
+
} else if (clampedFrameIndex == 1 || clampedFrameIndex == 2) {
|
|
990
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
991
|
+
} else if (clampedFrameIndex == 3) {
|
|
992
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
993
|
+
} else if (clampedFrameIndex == 4) {
|
|
994
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
995
|
+
} else if (clampedFrameIndex == 5) {
|
|
996
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
const onMouseDown = (e) => {
|
|
1000
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
1001
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1002
|
+
const touchGap = e.clientX - rect.left;
|
|
1003
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
1004
|
+
setThirdFrameProgress(initialProgress);
|
|
1005
|
+
setShowSwipeHint(false);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const onMouseMove = (e) => {
|
|
1009
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
1010
|
+
const canvas = canvasRef.current;
|
|
1011
|
+
const rect = canvas.getBoundingClientRect();
|
|
1012
|
+
const currentX = e.clientX;
|
|
1013
|
+
const relativeX = currentX - rect.left;
|
|
1014
|
+
const canvasWidth = rect.width;
|
|
1015
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1016
|
+
setThirdFrameProgress(progress);
|
|
1017
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1018
|
+
hasTriggeredFourthFrame.current = true;
|
|
1019
|
+
setIsPausedAtThird(false);
|
|
1020
|
+
setThirdFrameProgress(0);
|
|
1021
|
+
setFrame(3);
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
const onMouseUp = (e) => {
|
|
1025
|
+
if (isPausedAtThird) {
|
|
1026
|
+
setThirdFrameProgress(0);
|
|
1027
|
+
setShowSwipeHint(true);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1031
|
+
isAnimating && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative w-full md:w-[85%] mx-auto", children: [
|
|
1032
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1033
|
+
"canvas",
|
|
1034
|
+
{
|
|
1035
|
+
ref: canvasRef,
|
|
1036
|
+
onMouseDown,
|
|
1037
|
+
onMouseMove,
|
|
1038
|
+
onMouseUp,
|
|
1039
|
+
onMouseLeave: onMouseUp,
|
|
1040
|
+
onTouchStart: (e) => {
|
|
1041
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
1042
|
+
const touch = e.touches[0];
|
|
1043
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1044
|
+
const touchGap = touch.clientX - rect.left;
|
|
1045
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
1046
|
+
setThirdFrameProgress(initialProgress);
|
|
1047
|
+
setShowSwipeHint(false);
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
onTouchMove: (e) => {
|
|
1051
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
1052
|
+
const touch = e.touches[0];
|
|
1053
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1054
|
+
const relativeX = touch.clientX - rect.left;
|
|
1055
|
+
const canvasWidth = rect.width;
|
|
1056
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1057
|
+
setThirdFrameProgress(progress);
|
|
1058
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1059
|
+
hasTriggeredFourthFrame.current = true;
|
|
1060
|
+
setIsPausedAtThird(false);
|
|
1061
|
+
setThirdFrameProgress(0);
|
|
1062
|
+
setFrame(3);
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
onTouchEnd: (e) => {
|
|
1066
|
+
if (isPausedAtThird) {
|
|
1067
|
+
e.preventDefault();
|
|
1068
|
+
setThirdFrameProgress(0);
|
|
1069
|
+
setShowSwipeHint(true);
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
className: "select-none cursor-pointer w-full h-full object-contain",
|
|
1073
|
+
style: {
|
|
1074
|
+
imageRendering: "pixelated",
|
|
1075
|
+
cursor: isPausedAtThird ? "grab" : "pointer",
|
|
1076
|
+
touchAction: "none",
|
|
1077
|
+
// 防止默認觸摸行為
|
|
1078
|
+
userSelect: "none"
|
|
1079
|
+
// 防止選中
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
),
|
|
1083
|
+
isPausedAtThird && showSwipeHint && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute bottom-8 left-1/2 transform -translate-x-1/2 bg-black/70 text-white px-6 py-3 rounded-full text-sm md:text-base font-medium animate-pulse pointer-events-none z-20", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1084
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1085
|
+
"svg",
|
|
1086
|
+
{
|
|
1087
|
+
className: "w-5 h-5 md:w-6 md:h-6",
|
|
1088
|
+
fill: "none",
|
|
1089
|
+
stroke: "currentColor",
|
|
1090
|
+
viewBox: "0 0 24 24",
|
|
1091
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1092
|
+
"path",
|
|
1093
|
+
{
|
|
1094
|
+
strokeLinecap: "round",
|
|
1095
|
+
strokeLinejoin: "round",
|
|
1096
|
+
strokeWidth: 2,
|
|
1097
|
+
d: "M13 7l5 5m0 0l-5 5m5-5H6"
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
),
|
|
1102
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: swipeHintText })
|
|
1103
|
+
] }) })
|
|
1104
|
+
] }),
|
|
1105
|
+
!isAnimating && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1106
|
+
"img",
|
|
1107
|
+
{
|
|
1108
|
+
src: mysteryBoxImage,
|
|
1109
|
+
alt: mysteryBoxAlt,
|
|
1110
|
+
className: "w-36 h-36 md:w-96 md:h-96 object-contain relative z-10"
|
|
1111
|
+
}
|
|
1112
|
+
)
|
|
1113
|
+
] });
|
|
1114
|
+
}
|
|
1115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1116
|
+
0 && (module.exports = {
|
|
1117
|
+
BagAnimation,
|
|
1118
|
+
defaultAnimationFrames,
|
|
1119
|
+
defaultImagePath,
|
|
1120
|
+
getAssetPath
|
|
1121
|
+
});
|
package/dist/index.mjs
CHANGED
|
@@ -769,4 +769,336 @@ var require_lib = __commonJS({
|
|
|
769
769
|
// src/BagAnimation.tsx
|
|
770
770
|
var import_apng_js = __toESM(require_lib());
|
|
771
771
|
import { useEffect, useRef, useState } from "react";
|
|
772
|
+
|
|
773
|
+
// src/assets.ts
|
|
774
|
+
function getAssetPath(relativePath) {
|
|
775
|
+
const cleanPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
|
|
776
|
+
return `/assets/${cleanPath}`;
|
|
777
|
+
}
|
|
778
|
+
var defaultAnimationFrames = [
|
|
779
|
+
"/assets/bag/blind_box_spin.png",
|
|
780
|
+
"/assets/bag/blind_box_unopen.png",
|
|
781
|
+
"/assets/bag/blind_box_tear_off.png",
|
|
782
|
+
"/assets/bag/blind_box_open.png"
|
|
783
|
+
];
|
|
784
|
+
var defaultImagePath = "/assets/bag.png";
|
|
785
|
+
|
|
786
|
+
// src/BagAnimation.tsx
|
|
772
787
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
788
|
+
function BagAnimation({
|
|
789
|
+
isAnimating,
|
|
790
|
+
setIsAnimating,
|
|
791
|
+
doneFunction,
|
|
792
|
+
frames = defaultAnimationFrames,
|
|
793
|
+
defaultImage = defaultImagePath,
|
|
794
|
+
defaultImageAlt = "Weedza Mystery Box",
|
|
795
|
+
swipeHintText = "Swipe to open"
|
|
796
|
+
}) {
|
|
797
|
+
const [frame, setFrame] = useState(0);
|
|
798
|
+
const [isPausedAtThird, setIsPausedAtThird] = useState(false);
|
|
799
|
+
const [thirdFrameProgress, setThirdFrameProgress] = useState(0);
|
|
800
|
+
const [showSwipeHint, setShowSwipeHint] = useState(true);
|
|
801
|
+
const canvasRef = useRef(null);
|
|
802
|
+
const apngRefs = useRef([]);
|
|
803
|
+
const animationRefs = useRef([]);
|
|
804
|
+
const thirdFramePlayerRef = useRef(null);
|
|
805
|
+
const thirdFrameCtxRef = useRef(null);
|
|
806
|
+
const thirdFrameApngRef = useRef(null);
|
|
807
|
+
const thirdFrameOriginalSizeRef = useRef(null);
|
|
808
|
+
const hasTriggeredFourthFrame = useRef(false);
|
|
809
|
+
const mysteryBoxImage = defaultImage;
|
|
810
|
+
const mysteryBoxAlt = defaultImageAlt;
|
|
811
|
+
useEffect(() => {
|
|
812
|
+
if (isAnimating) {
|
|
813
|
+
setFrame(0);
|
|
814
|
+
setIsPausedAtThird(false);
|
|
815
|
+
setThirdFrameProgress(0);
|
|
816
|
+
setShowSwipeHint(true);
|
|
817
|
+
hasTriggeredFourthFrame.current = false;
|
|
818
|
+
} else {
|
|
819
|
+
animationRefs.current.forEach((anim) => {
|
|
820
|
+
if (anim) anim.stop();
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}, [isAnimating]);
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
if (!isAnimating || !canvasRef.current) return;
|
|
826
|
+
const canvas = canvasRef.current;
|
|
827
|
+
const ctx = canvas.getContext("2d");
|
|
828
|
+
if (!ctx) return;
|
|
829
|
+
if (animationRefs.current[frame]) {
|
|
830
|
+
animationRefs.current[frame].stop();
|
|
831
|
+
}
|
|
832
|
+
const loadAPNG = async () => {
|
|
833
|
+
try {
|
|
834
|
+
const response = await fetch(frames[frame]);
|
|
835
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
836
|
+
const apng = await (0, import_apng_js.default)(arrayBuffer);
|
|
837
|
+
if (apng instanceof Error) {
|
|
838
|
+
console.error("Failed to parse APNG:", apng);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
canvas.width = apng.width;
|
|
842
|
+
canvas.height = apng.height;
|
|
843
|
+
apngRefs.current[frame] = apng;
|
|
844
|
+
await apng.createImages();
|
|
845
|
+
const player = await apng.getPlayer(ctx);
|
|
846
|
+
let isStopped = false;
|
|
847
|
+
let stopFunction = null;
|
|
848
|
+
let totalDuration = 0;
|
|
849
|
+
for (let i = 0; i < apng.frames.length; i++) {
|
|
850
|
+
const frameDelay = apng.frames[i].delay || 100;
|
|
851
|
+
totalDuration += frameDelay / 1e3 * 1e3;
|
|
852
|
+
}
|
|
853
|
+
player.play(1);
|
|
854
|
+
if (player.stop) {
|
|
855
|
+
stopFunction = player.stop.bind(player);
|
|
856
|
+
}
|
|
857
|
+
animationRefs.current[frame] = {
|
|
858
|
+
stop: () => {
|
|
859
|
+
isStopped = true;
|
|
860
|
+
if (stopFunction) {
|
|
861
|
+
stopFunction();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
if (frame === 2) {
|
|
866
|
+
if (stopFunction) {
|
|
867
|
+
stopFunction();
|
|
868
|
+
}
|
|
869
|
+
thirdFramePlayerRef.current = player;
|
|
870
|
+
thirdFrameCtxRef.current = ctx;
|
|
871
|
+
thirdFrameApngRef.current = apng;
|
|
872
|
+
thirdFrameOriginalSizeRef.current = {
|
|
873
|
+
width: apng.width,
|
|
874
|
+
height: apng.height
|
|
875
|
+
};
|
|
876
|
+
setTimeout(() => {
|
|
877
|
+
setIsPausedAtThird(true);
|
|
878
|
+
setThirdFrameProgress(0);
|
|
879
|
+
}, 0);
|
|
880
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
881
|
+
animationRefs.current[frame].stop = () => {
|
|
882
|
+
isStopped = true;
|
|
883
|
+
if (stopFunction) {
|
|
884
|
+
stopFunction();
|
|
885
|
+
}
|
|
886
|
+
originalStop();
|
|
887
|
+
};
|
|
888
|
+
} else {
|
|
889
|
+
const timeoutId = setTimeout(() => {
|
|
890
|
+
if (!isStopped) {
|
|
891
|
+
if (frame < frames.length - 1) {
|
|
892
|
+
setFrame(frame + 1);
|
|
893
|
+
} else {
|
|
894
|
+
setIsAnimating(false);
|
|
895
|
+
doneFunction();
|
|
896
|
+
setFrame(0);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}, totalDuration);
|
|
900
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
901
|
+
animationRefs.current[frame].stop = () => {
|
|
902
|
+
isStopped = true;
|
|
903
|
+
clearTimeout(timeoutId);
|
|
904
|
+
if (stopFunction) {
|
|
905
|
+
stopFunction();
|
|
906
|
+
}
|
|
907
|
+
originalStop();
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
} catch (error) {
|
|
911
|
+
console.error("Error loading APNG:", error);
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
loadAPNG();
|
|
915
|
+
return () => {
|
|
916
|
+
if (animationRefs.current[frame]) {
|
|
917
|
+
animationRefs.current[frame].stop();
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
}, [frame, isAnimating, setIsAnimating, doneFunction]);
|
|
921
|
+
useEffect(() => {
|
|
922
|
+
if (isPausedAtThird && thirdFrameCtxRef.current && thirdFrameApngRef.current && thirdFramePlayerRef.current && canvasRef.current && thirdFrameOriginalSizeRef.current) {
|
|
923
|
+
const apng = thirdFrameApngRef.current;
|
|
924
|
+
const ctx = thirdFrameCtxRef.current;
|
|
925
|
+
const canvas = canvasRef.current;
|
|
926
|
+
const player = thirdFramePlayerRef.current;
|
|
927
|
+
const originalSize = thirdFrameOriginalSizeRef.current;
|
|
928
|
+
canvas.width = originalSize.width;
|
|
929
|
+
canvas.height = originalSize.height;
|
|
930
|
+
const totalFrames = apng.frames.length;
|
|
931
|
+
const targetFrameIndex = Math.floor(thirdFrameProgress / 100 * totalFrames);
|
|
932
|
+
const clampedFrameIndex = Math.min(Math.max(0, targetFrameIndex), totalFrames - 1);
|
|
933
|
+
if (apng.frames[clampedFrameIndex]) {
|
|
934
|
+
try {
|
|
935
|
+
ctx.save();
|
|
936
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
937
|
+
ctx.clearRect(0, 0, originalSize.width, originalSize.height);
|
|
938
|
+
if (player.render && typeof player.render === "function") {
|
|
939
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
940
|
+
canvas.width = originalSize.width;
|
|
941
|
+
canvas.height = originalSize.height;
|
|
942
|
+
}
|
|
943
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
944
|
+
} else {
|
|
945
|
+
const frame2 = apng.frames[clampedFrameIndex];
|
|
946
|
+
if (frame2.imageElement && frame2.imageElement instanceof HTMLImageElement) {
|
|
947
|
+
const img = frame2.imageElement;
|
|
948
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
949
|
+
} else if (frame2.image && frame2.image instanceof HTMLImageElement) {
|
|
950
|
+
const img = frame2.image;
|
|
951
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
952
|
+
} else {
|
|
953
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
954
|
+
canvas.width = originalSize.width;
|
|
955
|
+
canvas.height = originalSize.height;
|
|
956
|
+
}
|
|
957
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
ctx.restore();
|
|
961
|
+
} catch (error) {
|
|
962
|
+
console.error("Error rendering frame:", error);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}, [thirdFrameProgress, isPausedAtThird]);
|
|
967
|
+
const ctxDrawSwipeFrames = (ctx, img, clampedFrameIndex, originalSize) => {
|
|
968
|
+
let buttonGapPercentage = 0;
|
|
969
|
+
let startX = originalSize.width * 0.30685;
|
|
970
|
+
let startY = originalSize.height - (originalSize.height * buttonGapPercentage + img.height);
|
|
971
|
+
if (clampedFrameIndex == 0) {
|
|
972
|
+
ctx.drawImage(img, 0, 0, originalSize.width, originalSize.height);
|
|
973
|
+
} else if (clampedFrameIndex == 1 || clampedFrameIndex == 2) {
|
|
974
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
975
|
+
} else if (clampedFrameIndex == 3) {
|
|
976
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
977
|
+
} else if (clampedFrameIndex == 4) {
|
|
978
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
979
|
+
} else if (clampedFrameIndex == 5) {
|
|
980
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
const onMouseDown = (e) => {
|
|
984
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
985
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
986
|
+
const touchGap = e.clientX - rect.left;
|
|
987
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
988
|
+
setThirdFrameProgress(initialProgress);
|
|
989
|
+
setShowSwipeHint(false);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const onMouseMove = (e) => {
|
|
993
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
994
|
+
const canvas = canvasRef.current;
|
|
995
|
+
const rect = canvas.getBoundingClientRect();
|
|
996
|
+
const currentX = e.clientX;
|
|
997
|
+
const relativeX = currentX - rect.left;
|
|
998
|
+
const canvasWidth = rect.width;
|
|
999
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1000
|
+
setThirdFrameProgress(progress);
|
|
1001
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1002
|
+
hasTriggeredFourthFrame.current = true;
|
|
1003
|
+
setIsPausedAtThird(false);
|
|
1004
|
+
setThirdFrameProgress(0);
|
|
1005
|
+
setFrame(3);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
const onMouseUp = (e) => {
|
|
1009
|
+
if (isPausedAtThird) {
|
|
1010
|
+
setThirdFrameProgress(0);
|
|
1011
|
+
setShowSwipeHint(true);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1015
|
+
isAnimating && /* @__PURE__ */ jsxs("div", { className: "relative w-full md:w-[85%] mx-auto", children: [
|
|
1016
|
+
/* @__PURE__ */ jsx(
|
|
1017
|
+
"canvas",
|
|
1018
|
+
{
|
|
1019
|
+
ref: canvasRef,
|
|
1020
|
+
onMouseDown,
|
|
1021
|
+
onMouseMove,
|
|
1022
|
+
onMouseUp,
|
|
1023
|
+
onMouseLeave: onMouseUp,
|
|
1024
|
+
onTouchStart: (e) => {
|
|
1025
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
1026
|
+
const touch = e.touches[0];
|
|
1027
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1028
|
+
const touchGap = touch.clientX - rect.left;
|
|
1029
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
1030
|
+
setThirdFrameProgress(initialProgress);
|
|
1031
|
+
setShowSwipeHint(false);
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
onTouchMove: (e) => {
|
|
1035
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
1036
|
+
const touch = e.touches[0];
|
|
1037
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1038
|
+
const relativeX = touch.clientX - rect.left;
|
|
1039
|
+
const canvasWidth = rect.width;
|
|
1040
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1041
|
+
setThirdFrameProgress(progress);
|
|
1042
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1043
|
+
hasTriggeredFourthFrame.current = true;
|
|
1044
|
+
setIsPausedAtThird(false);
|
|
1045
|
+
setThirdFrameProgress(0);
|
|
1046
|
+
setFrame(3);
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
onTouchEnd: (e) => {
|
|
1050
|
+
if (isPausedAtThird) {
|
|
1051
|
+
e.preventDefault();
|
|
1052
|
+
setThirdFrameProgress(0);
|
|
1053
|
+
setShowSwipeHint(true);
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
className: "select-none cursor-pointer w-full h-full object-contain",
|
|
1057
|
+
style: {
|
|
1058
|
+
imageRendering: "pixelated",
|
|
1059
|
+
cursor: isPausedAtThird ? "grab" : "pointer",
|
|
1060
|
+
touchAction: "none",
|
|
1061
|
+
// 防止默認觸摸行為
|
|
1062
|
+
userSelect: "none"
|
|
1063
|
+
// 防止選中
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
),
|
|
1067
|
+
isPausedAtThird && showSwipeHint && /* @__PURE__ */ jsx("div", { className: "absolute bottom-8 left-1/2 transform -translate-x-1/2 bg-black/70 text-white px-6 py-3 rounded-full text-sm md:text-base font-medium animate-pulse pointer-events-none z-20", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1068
|
+
/* @__PURE__ */ jsx(
|
|
1069
|
+
"svg",
|
|
1070
|
+
{
|
|
1071
|
+
className: "w-5 h-5 md:w-6 md:h-6",
|
|
1072
|
+
fill: "none",
|
|
1073
|
+
stroke: "currentColor",
|
|
1074
|
+
viewBox: "0 0 24 24",
|
|
1075
|
+
children: /* @__PURE__ */ jsx(
|
|
1076
|
+
"path",
|
|
1077
|
+
{
|
|
1078
|
+
strokeLinecap: "round",
|
|
1079
|
+
strokeLinejoin: "round",
|
|
1080
|
+
strokeWidth: 2,
|
|
1081
|
+
d: "M13 7l5 5m0 0l-5 5m5-5H6"
|
|
1082
|
+
}
|
|
1083
|
+
)
|
|
1084
|
+
}
|
|
1085
|
+
),
|
|
1086
|
+
/* @__PURE__ */ jsx("span", { children: swipeHintText })
|
|
1087
|
+
] }) })
|
|
1088
|
+
] }),
|
|
1089
|
+
!isAnimating && /* @__PURE__ */ jsx(
|
|
1090
|
+
"img",
|
|
1091
|
+
{
|
|
1092
|
+
src: mysteryBoxImage,
|
|
1093
|
+
alt: mysteryBoxAlt,
|
|
1094
|
+
className: "w-36 h-36 md:w-96 md:h-96 object-contain relative z-10"
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
] });
|
|
1098
|
+
}
|
|
1099
|
+
export {
|
|
1100
|
+
BagAnimation,
|
|
1101
|
+
defaultAnimationFrames,
|
|
1102
|
+
defaultImagePath,
|
|
1103
|
+
getAssetPath
|
|
1104
|
+
};
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chao-component/bag-animation-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
9
10
|
"import": "./dist/index.mjs",
|
|
10
|
-
"require": "./dist/index.js"
|
|
11
|
-
"types": "./dist/index.d.ts"
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"directories": {
|
|
18
|
+
"assets": "dist/assets"
|
|
19
|
+
},
|
|
15
20
|
"scripts": {
|
|
16
21
|
"build": "tsup",
|
|
17
22
|
"dev": "tsup --watch"
|