@chao-component/bag-animation-ui 1.0.0 → 1.0.1
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/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +320 -0
- package/dist/index.mjs +312 -0
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
declare function BagAnimation({ isAnimating, setIsAnimating, doneFunction }: {
|
|
4
|
+
isAnimating: boolean;
|
|
5
|
+
setIsAnimating: (isAnimating: boolean) => void;
|
|
6
|
+
doneFunction: () => void;
|
|
7
|
+
}): react_jsx_runtime.JSX.Element;
|
|
8
|
+
|
|
9
|
+
export { BagAnimation };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
declare function BagAnimation({ isAnimating, setIsAnimating, doneFunction }: {
|
|
4
|
+
isAnimating: boolean;
|
|
5
|
+
setIsAnimating: (isAnimating: boolean) => void;
|
|
6
|
+
doneFunction: () => void;
|
|
7
|
+
}): react_jsx_runtime.JSX.Element;
|
|
8
|
+
|
|
9
|
+
export { BagAnimation };
|
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,325 @@ var require_lib = __commonJS({
|
|
|
770
774
|
|
|
771
775
|
// src/index.ts
|
|
772
776
|
var index_exports = {};
|
|
777
|
+
__export(index_exports, {
|
|
778
|
+
BagAnimation: () => BagAnimation
|
|
779
|
+
});
|
|
773
780
|
module.exports = __toCommonJS(index_exports);
|
|
774
781
|
|
|
775
782
|
// src/BagAnimation.tsx
|
|
776
783
|
var import_react = require("react");
|
|
777
784
|
var import_apng_js = __toESM(require_lib());
|
|
778
785
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
786
|
+
var frames = [
|
|
787
|
+
"/assets/main/bag/blind_box_spin.png",
|
|
788
|
+
"/assets/main/bag/blind_box_unopen.png",
|
|
789
|
+
"/assets/main/bag/blind_box_tear_off.png",
|
|
790
|
+
"/assets/main/bag/blind_box_open.png"
|
|
791
|
+
];
|
|
792
|
+
function BagAnimation({ isAnimating, setIsAnimating, doneFunction }) {
|
|
793
|
+
const [frame, setFrame] = (0, import_react.useState)(0);
|
|
794
|
+
const [isPausedAtThird, setIsPausedAtThird] = (0, import_react.useState)(false);
|
|
795
|
+
const [thirdFrameProgress, setThirdFrameProgress] = (0, import_react.useState)(0);
|
|
796
|
+
const [showSwipeHint, setShowSwipeHint] = (0, import_react.useState)(true);
|
|
797
|
+
const canvasRef = (0, import_react.useRef)(null);
|
|
798
|
+
const apngRefs = (0, import_react.useRef)([]);
|
|
799
|
+
const animationRefs = (0, import_react.useRef)([]);
|
|
800
|
+
const thirdFramePlayerRef = (0, import_react.useRef)(null);
|
|
801
|
+
const thirdFrameCtxRef = (0, import_react.useRef)(null);
|
|
802
|
+
const thirdFrameApngRef = (0, import_react.useRef)(null);
|
|
803
|
+
const thirdFrameOriginalSizeRef = (0, import_react.useRef)(null);
|
|
804
|
+
const hasTriggeredFourthFrame = (0, import_react.useRef)(false);
|
|
805
|
+
const mysteryBoxImage = "/assets/main/bag.png";
|
|
806
|
+
const mysteryBoxAlt = "Weedza Mystery Box";
|
|
807
|
+
(0, import_react.useEffect)(() => {
|
|
808
|
+
if (isAnimating) {
|
|
809
|
+
setFrame(0);
|
|
810
|
+
setIsPausedAtThird(false);
|
|
811
|
+
setThirdFrameProgress(0);
|
|
812
|
+
setShowSwipeHint(true);
|
|
813
|
+
hasTriggeredFourthFrame.current = false;
|
|
814
|
+
} else {
|
|
815
|
+
animationRefs.current.forEach((anim) => {
|
|
816
|
+
if (anim) anim.stop();
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}, [isAnimating]);
|
|
820
|
+
(0, import_react.useEffect)(() => {
|
|
821
|
+
if (!isAnimating || !canvasRef.current) return;
|
|
822
|
+
const canvas = canvasRef.current;
|
|
823
|
+
const ctx = canvas.getContext("2d");
|
|
824
|
+
if (!ctx) return;
|
|
825
|
+
if (animationRefs.current[frame]) {
|
|
826
|
+
animationRefs.current[frame].stop();
|
|
827
|
+
}
|
|
828
|
+
const loadAPNG = async () => {
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch(frames[frame]);
|
|
831
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
832
|
+
const apng = await (0, import_apng_js.default)(arrayBuffer);
|
|
833
|
+
if (apng instanceof Error) {
|
|
834
|
+
console.error("Failed to parse APNG:", apng);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
canvas.width = apng.width;
|
|
838
|
+
canvas.height = apng.height;
|
|
839
|
+
apngRefs.current[frame] = apng;
|
|
840
|
+
await apng.createImages();
|
|
841
|
+
const player = await apng.getPlayer(ctx);
|
|
842
|
+
let isStopped = false;
|
|
843
|
+
let stopFunction = null;
|
|
844
|
+
let totalDuration = 0;
|
|
845
|
+
for (let i = 0; i < apng.frames.length; i++) {
|
|
846
|
+
const frameDelay = apng.frames[i].delay || 100;
|
|
847
|
+
totalDuration += frameDelay / 1e3 * 1e3;
|
|
848
|
+
}
|
|
849
|
+
player.play(1);
|
|
850
|
+
if (player.stop) {
|
|
851
|
+
stopFunction = player.stop.bind(player);
|
|
852
|
+
}
|
|
853
|
+
animationRefs.current[frame] = {
|
|
854
|
+
stop: () => {
|
|
855
|
+
isStopped = true;
|
|
856
|
+
if (stopFunction) {
|
|
857
|
+
stopFunction();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
if (frame === 2) {
|
|
862
|
+
if (stopFunction) {
|
|
863
|
+
stopFunction();
|
|
864
|
+
}
|
|
865
|
+
thirdFramePlayerRef.current = player;
|
|
866
|
+
thirdFrameCtxRef.current = ctx;
|
|
867
|
+
thirdFrameApngRef.current = apng;
|
|
868
|
+
thirdFrameOriginalSizeRef.current = {
|
|
869
|
+
width: apng.width,
|
|
870
|
+
height: apng.height
|
|
871
|
+
};
|
|
872
|
+
setTimeout(() => {
|
|
873
|
+
setIsPausedAtThird(true);
|
|
874
|
+
setThirdFrameProgress(0);
|
|
875
|
+
}, 0);
|
|
876
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
877
|
+
animationRefs.current[frame].stop = () => {
|
|
878
|
+
isStopped = true;
|
|
879
|
+
if (stopFunction) {
|
|
880
|
+
stopFunction();
|
|
881
|
+
}
|
|
882
|
+
originalStop();
|
|
883
|
+
};
|
|
884
|
+
} else {
|
|
885
|
+
const timeoutId = setTimeout(() => {
|
|
886
|
+
if (!isStopped) {
|
|
887
|
+
if (frame < frames.length - 1) {
|
|
888
|
+
setFrame(frame + 1);
|
|
889
|
+
} else {
|
|
890
|
+
setIsAnimating(false);
|
|
891
|
+
doneFunction();
|
|
892
|
+
setFrame(0);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}, totalDuration);
|
|
896
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
897
|
+
animationRefs.current[frame].stop = () => {
|
|
898
|
+
isStopped = true;
|
|
899
|
+
clearTimeout(timeoutId);
|
|
900
|
+
if (stopFunction) {
|
|
901
|
+
stopFunction();
|
|
902
|
+
}
|
|
903
|
+
originalStop();
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error("Error loading APNG:", error);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
loadAPNG();
|
|
911
|
+
return () => {
|
|
912
|
+
if (animationRefs.current[frame]) {
|
|
913
|
+
animationRefs.current[frame].stop();
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}, [frame, isAnimating, setIsAnimating, doneFunction]);
|
|
917
|
+
(0, import_react.useEffect)(() => {
|
|
918
|
+
if (isPausedAtThird && thirdFrameCtxRef.current && thirdFrameApngRef.current && thirdFramePlayerRef.current && canvasRef.current && thirdFrameOriginalSizeRef.current) {
|
|
919
|
+
const apng = thirdFrameApngRef.current;
|
|
920
|
+
const ctx = thirdFrameCtxRef.current;
|
|
921
|
+
const canvas = canvasRef.current;
|
|
922
|
+
const player = thirdFramePlayerRef.current;
|
|
923
|
+
const originalSize = thirdFrameOriginalSizeRef.current;
|
|
924
|
+
canvas.width = originalSize.width;
|
|
925
|
+
canvas.height = originalSize.height;
|
|
926
|
+
const totalFrames = apng.frames.length;
|
|
927
|
+
const targetFrameIndex = Math.floor(thirdFrameProgress / 100 * totalFrames);
|
|
928
|
+
const clampedFrameIndex = Math.min(Math.max(0, targetFrameIndex), totalFrames - 1);
|
|
929
|
+
if (apng.frames[clampedFrameIndex]) {
|
|
930
|
+
try {
|
|
931
|
+
ctx.save();
|
|
932
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
933
|
+
ctx.clearRect(0, 0, originalSize.width, originalSize.height);
|
|
934
|
+
if (player.render && typeof player.render === "function") {
|
|
935
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
936
|
+
canvas.width = originalSize.width;
|
|
937
|
+
canvas.height = originalSize.height;
|
|
938
|
+
}
|
|
939
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
940
|
+
} else {
|
|
941
|
+
const frame2 = apng.frames[clampedFrameIndex];
|
|
942
|
+
if (frame2.imageElement && frame2.imageElement instanceof HTMLImageElement) {
|
|
943
|
+
const img = frame2.imageElement;
|
|
944
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
945
|
+
} else if (frame2.image && frame2.image instanceof HTMLImageElement) {
|
|
946
|
+
const img = frame2.image;
|
|
947
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
948
|
+
} else {
|
|
949
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
950
|
+
canvas.width = originalSize.width;
|
|
951
|
+
canvas.height = originalSize.height;
|
|
952
|
+
}
|
|
953
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
ctx.restore();
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.error("Error rendering frame:", error);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}, [thirdFrameProgress, isPausedAtThird]);
|
|
963
|
+
const ctxDrawSwipeFrames = (ctx, img, clampedFrameIndex, originalSize) => {
|
|
964
|
+
let buttonGapPercentage = 0;
|
|
965
|
+
let startX = originalSize.width * 0.30685;
|
|
966
|
+
let startY = originalSize.height - (originalSize.height * buttonGapPercentage + img.height);
|
|
967
|
+
if (clampedFrameIndex == 0) {
|
|
968
|
+
ctx.drawImage(img, 0, 0, originalSize.width, originalSize.height);
|
|
969
|
+
} else if (clampedFrameIndex == 1 || clampedFrameIndex == 2) {
|
|
970
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
971
|
+
} else if (clampedFrameIndex == 3) {
|
|
972
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
973
|
+
} else if (clampedFrameIndex == 4) {
|
|
974
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
975
|
+
} else if (clampedFrameIndex == 5) {
|
|
976
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const onMouseDown = (e) => {
|
|
980
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
981
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
982
|
+
const touchGap = e.clientX - rect.left;
|
|
983
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
984
|
+
setThirdFrameProgress(initialProgress);
|
|
985
|
+
setShowSwipeHint(false);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
const onMouseMove = (e) => {
|
|
989
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
990
|
+
const canvas = canvasRef.current;
|
|
991
|
+
const rect = canvas.getBoundingClientRect();
|
|
992
|
+
const currentX = e.clientX;
|
|
993
|
+
const relativeX = currentX - rect.left;
|
|
994
|
+
const canvasWidth = rect.width;
|
|
995
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
996
|
+
setThirdFrameProgress(progress);
|
|
997
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
998
|
+
hasTriggeredFourthFrame.current = true;
|
|
999
|
+
setIsPausedAtThird(false);
|
|
1000
|
+
setThirdFrameProgress(0);
|
|
1001
|
+
setFrame(3);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const onMouseUp = (e) => {
|
|
1005
|
+
if (isPausedAtThird) {
|
|
1006
|
+
setThirdFrameProgress(0);
|
|
1007
|
+
setShowSwipeHint(true);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
1011
|
+
isAnimating && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative w-full md:w-[85%] mx-auto", children: [
|
|
1012
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1013
|
+
"canvas",
|
|
1014
|
+
{
|
|
1015
|
+
ref: canvasRef,
|
|
1016
|
+
onMouseDown,
|
|
1017
|
+
onMouseMove,
|
|
1018
|
+
onMouseUp,
|
|
1019
|
+
onMouseLeave: onMouseUp,
|
|
1020
|
+
onTouchStart: (e) => {
|
|
1021
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
1022
|
+
const touch = e.touches[0];
|
|
1023
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1024
|
+
const touchGap = touch.clientX - rect.left;
|
|
1025
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
1026
|
+
setThirdFrameProgress(initialProgress);
|
|
1027
|
+
setShowSwipeHint(false);
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
onTouchMove: (e) => {
|
|
1031
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
1032
|
+
const touch = e.touches[0];
|
|
1033
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1034
|
+
const relativeX = touch.clientX - rect.left;
|
|
1035
|
+
const canvasWidth = rect.width;
|
|
1036
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1037
|
+
setThirdFrameProgress(progress);
|
|
1038
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1039
|
+
hasTriggeredFourthFrame.current = true;
|
|
1040
|
+
setIsPausedAtThird(false);
|
|
1041
|
+
setThirdFrameProgress(0);
|
|
1042
|
+
setFrame(3);
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
onTouchEnd: (e) => {
|
|
1046
|
+
if (isPausedAtThird) {
|
|
1047
|
+
e.preventDefault();
|
|
1048
|
+
setThirdFrameProgress(0);
|
|
1049
|
+
setShowSwipeHint(true);
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
className: "select-none cursor-pointer w-full h-full object-contain",
|
|
1053
|
+
style: {
|
|
1054
|
+
imageRendering: "pixelated",
|
|
1055
|
+
cursor: isPausedAtThird ? "grab" : "pointer",
|
|
1056
|
+
touchAction: "none",
|
|
1057
|
+
// 防止默認觸摸行為
|
|
1058
|
+
userSelect: "none"
|
|
1059
|
+
// 防止選中
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
),
|
|
1063
|
+
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: [
|
|
1064
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1065
|
+
"svg",
|
|
1066
|
+
{
|
|
1067
|
+
className: "w-5 h-5 md:w-6 md:h-6",
|
|
1068
|
+
fill: "none",
|
|
1069
|
+
stroke: "currentColor",
|
|
1070
|
+
viewBox: "0 0 24 24",
|
|
1071
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1072
|
+
"path",
|
|
1073
|
+
{
|
|
1074
|
+
strokeLinecap: "round",
|
|
1075
|
+
strokeLinejoin: "round",
|
|
1076
|
+
strokeWidth: 2,
|
|
1077
|
+
d: "M13 7l5 5m0 0l-5 5m5-5H6"
|
|
1078
|
+
}
|
|
1079
|
+
)
|
|
1080
|
+
}
|
|
1081
|
+
),
|
|
1082
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Swipe to open" })
|
|
1083
|
+
] }) })
|
|
1084
|
+
] }),
|
|
1085
|
+
!isAnimating && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1086
|
+
"img",
|
|
1087
|
+
{
|
|
1088
|
+
src: mysteryBoxImage,
|
|
1089
|
+
alt: mysteryBoxAlt,
|
|
1090
|
+
className: "w-36 h-36 md:w-96 md:h-96 object-contain relative z-10"
|
|
1091
|
+
}
|
|
1092
|
+
)
|
|
1093
|
+
] });
|
|
1094
|
+
}
|
|
1095
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1096
|
+
0 && (module.exports = {
|
|
1097
|
+
BagAnimation
|
|
1098
|
+
});
|
package/dist/index.mjs
CHANGED
|
@@ -770,3 +770,315 @@ var require_lib = __commonJS({
|
|
|
770
770
|
var import_apng_js = __toESM(require_lib());
|
|
771
771
|
import { useEffect, useRef, useState } from "react";
|
|
772
772
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
773
|
+
var frames = [
|
|
774
|
+
"/assets/main/bag/blind_box_spin.png",
|
|
775
|
+
"/assets/main/bag/blind_box_unopen.png",
|
|
776
|
+
"/assets/main/bag/blind_box_tear_off.png",
|
|
777
|
+
"/assets/main/bag/blind_box_open.png"
|
|
778
|
+
];
|
|
779
|
+
function BagAnimation({ isAnimating, setIsAnimating, doneFunction }) {
|
|
780
|
+
const [frame, setFrame] = useState(0);
|
|
781
|
+
const [isPausedAtThird, setIsPausedAtThird] = useState(false);
|
|
782
|
+
const [thirdFrameProgress, setThirdFrameProgress] = useState(0);
|
|
783
|
+
const [showSwipeHint, setShowSwipeHint] = useState(true);
|
|
784
|
+
const canvasRef = useRef(null);
|
|
785
|
+
const apngRefs = useRef([]);
|
|
786
|
+
const animationRefs = useRef([]);
|
|
787
|
+
const thirdFramePlayerRef = useRef(null);
|
|
788
|
+
const thirdFrameCtxRef = useRef(null);
|
|
789
|
+
const thirdFrameApngRef = useRef(null);
|
|
790
|
+
const thirdFrameOriginalSizeRef = useRef(null);
|
|
791
|
+
const hasTriggeredFourthFrame = useRef(false);
|
|
792
|
+
const mysteryBoxImage = "/assets/main/bag.png";
|
|
793
|
+
const mysteryBoxAlt = "Weedza Mystery Box";
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
if (isAnimating) {
|
|
796
|
+
setFrame(0);
|
|
797
|
+
setIsPausedAtThird(false);
|
|
798
|
+
setThirdFrameProgress(0);
|
|
799
|
+
setShowSwipeHint(true);
|
|
800
|
+
hasTriggeredFourthFrame.current = false;
|
|
801
|
+
} else {
|
|
802
|
+
animationRefs.current.forEach((anim) => {
|
|
803
|
+
if (anim) anim.stop();
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}, [isAnimating]);
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
if (!isAnimating || !canvasRef.current) return;
|
|
809
|
+
const canvas = canvasRef.current;
|
|
810
|
+
const ctx = canvas.getContext("2d");
|
|
811
|
+
if (!ctx) return;
|
|
812
|
+
if (animationRefs.current[frame]) {
|
|
813
|
+
animationRefs.current[frame].stop();
|
|
814
|
+
}
|
|
815
|
+
const loadAPNG = async () => {
|
|
816
|
+
try {
|
|
817
|
+
const response = await fetch(frames[frame]);
|
|
818
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
819
|
+
const apng = await (0, import_apng_js.default)(arrayBuffer);
|
|
820
|
+
if (apng instanceof Error) {
|
|
821
|
+
console.error("Failed to parse APNG:", apng);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
canvas.width = apng.width;
|
|
825
|
+
canvas.height = apng.height;
|
|
826
|
+
apngRefs.current[frame] = apng;
|
|
827
|
+
await apng.createImages();
|
|
828
|
+
const player = await apng.getPlayer(ctx);
|
|
829
|
+
let isStopped = false;
|
|
830
|
+
let stopFunction = null;
|
|
831
|
+
let totalDuration = 0;
|
|
832
|
+
for (let i = 0; i < apng.frames.length; i++) {
|
|
833
|
+
const frameDelay = apng.frames[i].delay || 100;
|
|
834
|
+
totalDuration += frameDelay / 1e3 * 1e3;
|
|
835
|
+
}
|
|
836
|
+
player.play(1);
|
|
837
|
+
if (player.stop) {
|
|
838
|
+
stopFunction = player.stop.bind(player);
|
|
839
|
+
}
|
|
840
|
+
animationRefs.current[frame] = {
|
|
841
|
+
stop: () => {
|
|
842
|
+
isStopped = true;
|
|
843
|
+
if (stopFunction) {
|
|
844
|
+
stopFunction();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
if (frame === 2) {
|
|
849
|
+
if (stopFunction) {
|
|
850
|
+
stopFunction();
|
|
851
|
+
}
|
|
852
|
+
thirdFramePlayerRef.current = player;
|
|
853
|
+
thirdFrameCtxRef.current = ctx;
|
|
854
|
+
thirdFrameApngRef.current = apng;
|
|
855
|
+
thirdFrameOriginalSizeRef.current = {
|
|
856
|
+
width: apng.width,
|
|
857
|
+
height: apng.height
|
|
858
|
+
};
|
|
859
|
+
setTimeout(() => {
|
|
860
|
+
setIsPausedAtThird(true);
|
|
861
|
+
setThirdFrameProgress(0);
|
|
862
|
+
}, 0);
|
|
863
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
864
|
+
animationRefs.current[frame].stop = () => {
|
|
865
|
+
isStopped = true;
|
|
866
|
+
if (stopFunction) {
|
|
867
|
+
stopFunction();
|
|
868
|
+
}
|
|
869
|
+
originalStop();
|
|
870
|
+
};
|
|
871
|
+
} else {
|
|
872
|
+
const timeoutId = setTimeout(() => {
|
|
873
|
+
if (!isStopped) {
|
|
874
|
+
if (frame < frames.length - 1) {
|
|
875
|
+
setFrame(frame + 1);
|
|
876
|
+
} else {
|
|
877
|
+
setIsAnimating(false);
|
|
878
|
+
doneFunction();
|
|
879
|
+
setFrame(0);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}, totalDuration);
|
|
883
|
+
const originalStop = animationRefs.current[frame].stop;
|
|
884
|
+
animationRefs.current[frame].stop = () => {
|
|
885
|
+
isStopped = true;
|
|
886
|
+
clearTimeout(timeoutId);
|
|
887
|
+
if (stopFunction) {
|
|
888
|
+
stopFunction();
|
|
889
|
+
}
|
|
890
|
+
originalStop();
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.error("Error loading APNG:", error);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
loadAPNG();
|
|
898
|
+
return () => {
|
|
899
|
+
if (animationRefs.current[frame]) {
|
|
900
|
+
animationRefs.current[frame].stop();
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
}, [frame, isAnimating, setIsAnimating, doneFunction]);
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
if (isPausedAtThird && thirdFrameCtxRef.current && thirdFrameApngRef.current && thirdFramePlayerRef.current && canvasRef.current && thirdFrameOriginalSizeRef.current) {
|
|
906
|
+
const apng = thirdFrameApngRef.current;
|
|
907
|
+
const ctx = thirdFrameCtxRef.current;
|
|
908
|
+
const canvas = canvasRef.current;
|
|
909
|
+
const player = thirdFramePlayerRef.current;
|
|
910
|
+
const originalSize = thirdFrameOriginalSizeRef.current;
|
|
911
|
+
canvas.width = originalSize.width;
|
|
912
|
+
canvas.height = originalSize.height;
|
|
913
|
+
const totalFrames = apng.frames.length;
|
|
914
|
+
const targetFrameIndex = Math.floor(thirdFrameProgress / 100 * totalFrames);
|
|
915
|
+
const clampedFrameIndex = Math.min(Math.max(0, targetFrameIndex), totalFrames - 1);
|
|
916
|
+
if (apng.frames[clampedFrameIndex]) {
|
|
917
|
+
try {
|
|
918
|
+
ctx.save();
|
|
919
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
920
|
+
ctx.clearRect(0, 0, originalSize.width, originalSize.height);
|
|
921
|
+
if (player.render && typeof player.render === "function") {
|
|
922
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
923
|
+
canvas.width = originalSize.width;
|
|
924
|
+
canvas.height = originalSize.height;
|
|
925
|
+
}
|
|
926
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
927
|
+
} else {
|
|
928
|
+
const frame2 = apng.frames[clampedFrameIndex];
|
|
929
|
+
if (frame2.imageElement && frame2.imageElement instanceof HTMLImageElement) {
|
|
930
|
+
const img = frame2.imageElement;
|
|
931
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
932
|
+
} else if (frame2.image && frame2.image instanceof HTMLImageElement) {
|
|
933
|
+
const img = frame2.image;
|
|
934
|
+
ctxDrawSwipeFrames(ctx, img, clampedFrameIndex, originalSize);
|
|
935
|
+
} else {
|
|
936
|
+
if (canvas.width !== originalSize.width || canvas.height !== originalSize.height) {
|
|
937
|
+
canvas.width = originalSize.width;
|
|
938
|
+
canvas.height = originalSize.height;
|
|
939
|
+
}
|
|
940
|
+
player.render(apng.frames[clampedFrameIndex]);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
ctx.restore();
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.error("Error rendering frame:", error);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}, [thirdFrameProgress, isPausedAtThird]);
|
|
950
|
+
const ctxDrawSwipeFrames = (ctx, img, clampedFrameIndex, originalSize) => {
|
|
951
|
+
let buttonGapPercentage = 0;
|
|
952
|
+
let startX = originalSize.width * 0.30685;
|
|
953
|
+
let startY = originalSize.height - (originalSize.height * buttonGapPercentage + img.height);
|
|
954
|
+
if (clampedFrameIndex == 0) {
|
|
955
|
+
ctx.drawImage(img, 0, 0, originalSize.width, originalSize.height);
|
|
956
|
+
} else if (clampedFrameIndex == 1 || clampedFrameIndex == 2) {
|
|
957
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
958
|
+
} else if (clampedFrameIndex == 3) {
|
|
959
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
960
|
+
} else if (clampedFrameIndex == 4) {
|
|
961
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
962
|
+
} else if (clampedFrameIndex == 5) {
|
|
963
|
+
ctx.drawImage(img, startX, startY, img.width, img.height);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
const onMouseDown = (e) => {
|
|
967
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
968
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
969
|
+
const touchGap = e.clientX - rect.left;
|
|
970
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
971
|
+
setThirdFrameProgress(initialProgress);
|
|
972
|
+
setShowSwipeHint(false);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
const onMouseMove = (e) => {
|
|
976
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
977
|
+
const canvas = canvasRef.current;
|
|
978
|
+
const rect = canvas.getBoundingClientRect();
|
|
979
|
+
const currentX = e.clientX;
|
|
980
|
+
const relativeX = currentX - rect.left;
|
|
981
|
+
const canvasWidth = rect.width;
|
|
982
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
983
|
+
setThirdFrameProgress(progress);
|
|
984
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
985
|
+
hasTriggeredFourthFrame.current = true;
|
|
986
|
+
setIsPausedAtThird(false);
|
|
987
|
+
setThirdFrameProgress(0);
|
|
988
|
+
setFrame(3);
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
const onMouseUp = (e) => {
|
|
992
|
+
if (isPausedAtThird) {
|
|
993
|
+
setThirdFrameProgress(0);
|
|
994
|
+
setShowSwipeHint(true);
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
998
|
+
isAnimating && /* @__PURE__ */ jsxs("div", { className: "relative w-full md:w-[85%] mx-auto", children: [
|
|
999
|
+
/* @__PURE__ */ jsx(
|
|
1000
|
+
"canvas",
|
|
1001
|
+
{
|
|
1002
|
+
ref: canvasRef,
|
|
1003
|
+
onMouseDown,
|
|
1004
|
+
onMouseMove,
|
|
1005
|
+
onMouseUp,
|
|
1006
|
+
onMouseLeave: onMouseUp,
|
|
1007
|
+
onTouchStart: (e) => {
|
|
1008
|
+
if (isPausedAtThird && canvasRef.current) {
|
|
1009
|
+
const touch = e.touches[0];
|
|
1010
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1011
|
+
const touchGap = touch.clientX - rect.left;
|
|
1012
|
+
const initialProgress = Math.max(0, Math.min(100, touchGap / rect.width * 100));
|
|
1013
|
+
setThirdFrameProgress(initialProgress);
|
|
1014
|
+
setShowSwipeHint(false);
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
onTouchMove: (e) => {
|
|
1018
|
+
if (!isPausedAtThird || !canvasRef.current || hasTriggeredFourthFrame.current) return;
|
|
1019
|
+
const touch = e.touches[0];
|
|
1020
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
1021
|
+
const relativeX = touch.clientX - rect.left;
|
|
1022
|
+
const canvasWidth = rect.width;
|
|
1023
|
+
const progress = Math.max(0, Math.min(100, relativeX / canvasWidth * 100));
|
|
1024
|
+
setThirdFrameProgress(progress);
|
|
1025
|
+
if (progress >= 95 && !hasTriggeredFourthFrame.current) {
|
|
1026
|
+
hasTriggeredFourthFrame.current = true;
|
|
1027
|
+
setIsPausedAtThird(false);
|
|
1028
|
+
setThirdFrameProgress(0);
|
|
1029
|
+
setFrame(3);
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
onTouchEnd: (e) => {
|
|
1033
|
+
if (isPausedAtThird) {
|
|
1034
|
+
e.preventDefault();
|
|
1035
|
+
setThirdFrameProgress(0);
|
|
1036
|
+
setShowSwipeHint(true);
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
className: "select-none cursor-pointer w-full h-full object-contain",
|
|
1040
|
+
style: {
|
|
1041
|
+
imageRendering: "pixelated",
|
|
1042
|
+
cursor: isPausedAtThird ? "grab" : "pointer",
|
|
1043
|
+
touchAction: "none",
|
|
1044
|
+
// 防止默認觸摸行為
|
|
1045
|
+
userSelect: "none"
|
|
1046
|
+
// 防止選中
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
),
|
|
1050
|
+
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: [
|
|
1051
|
+
/* @__PURE__ */ jsx(
|
|
1052
|
+
"svg",
|
|
1053
|
+
{
|
|
1054
|
+
className: "w-5 h-5 md:w-6 md:h-6",
|
|
1055
|
+
fill: "none",
|
|
1056
|
+
stroke: "currentColor",
|
|
1057
|
+
viewBox: "0 0 24 24",
|
|
1058
|
+
children: /* @__PURE__ */ jsx(
|
|
1059
|
+
"path",
|
|
1060
|
+
{
|
|
1061
|
+
strokeLinecap: "round",
|
|
1062
|
+
strokeLinejoin: "round",
|
|
1063
|
+
strokeWidth: 2,
|
|
1064
|
+
d: "M13 7l5 5m0 0l-5 5m5-5H6"
|
|
1065
|
+
}
|
|
1066
|
+
)
|
|
1067
|
+
}
|
|
1068
|
+
),
|
|
1069
|
+
/* @__PURE__ */ jsx("span", { children: "Swipe to open" })
|
|
1070
|
+
] }) })
|
|
1071
|
+
] }),
|
|
1072
|
+
!isAnimating && /* @__PURE__ */ jsx(
|
|
1073
|
+
"img",
|
|
1074
|
+
{
|
|
1075
|
+
src: mysteryBoxImage,
|
|
1076
|
+
alt: mysteryBoxAlt,
|
|
1077
|
+
className: "w-36 h-36 md:w-96 md:h-96 object-contain relative z-10"
|
|
1078
|
+
}
|
|
1079
|
+
)
|
|
1080
|
+
] });
|
|
1081
|
+
}
|
|
1082
|
+
export {
|
|
1083
|
+
BagAnimation
|
|
1084
|
+
};
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chao-component/bag-animation-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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
14
|
"files": ["dist"],
|