Commit 4af2045b authored by timel's avatar timel Committed by Chunchi Che

feat: add animation

parent dcf03b35
......@@ -14,7 +14,6 @@ import {
YesNoModal,
} from "./Message";
import Mat from "./PlayMat";
import { Test } from "./Test";
import { Mat as NewMat } from "./NewPlayMat";
import { Menu } from "./NewPlayMat/Menu";
......
section#mat {
.mat-card {
position: absolute;
left: 0;
top: 0;
// left: 50%;
// top: 50%;
--card-height: 100px;
height: var(--card-height);
aspect-ratio: var(--card-ratio);
......@@ -12,7 +12,8 @@ section#mat {
position: relative;
height: 100%;
width: 100%;
transform: rotateY(calc(var(--ry) * 1deg));
transform: translateZ(calc(var(--z) * 1px))
rotateY(calc(var(--ry) * 1deg));
.card-cover,
.card-back {
width: 100%;
......
This diff is collapsed.
export * from "./toField";
export * from "./toHand";
export * from "./toDeck";
export * from "./toOutside";
import { isMe, type CardType } from "@/stores";
import { SpringApi } from "./types";
import { matConfig } from "../../utils";
import { ygopro } from "@/api";
import { easings } from "@react-spring/web";
import { asyncStart } from "./utils";
const {
PLANE_ROTATE_X,
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
CARD_RATIO,
COL_GAP,
ROW_GAP,
HAND_MARGIN_TOP,
HAND_CARD_HEIGHT,
HAND_CIRCLE_CENTER_OFFSET_Y,
DECK_OFFSET_X,
DECK_OFFSET_Y,
DECK_ROTATE_Z,
DECK_CARD_HEIGHT,
} = matConfig;
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
export const moveToDeck = async (props: {
card: CardType;
api: SpringApi;
report: boolean;
}) => {
const { card, api, report } = props;
// report
const { zone, sequence, controller, xyzMonster, position } = card;
const rightX = DECK_OFFSET_X.value + 2 * (BLOCK_WIDTH.value + COL_GAP.value);
const leftX = -rightX;
const bottomY =
DECK_OFFSET_Y.value +
2 * BLOCK_HEIGHT_M.value +
BLOCK_HEIGHT_S.value +
2 * ROW_GAP.value -
BLOCK_HEIGHT_S.value;
const topY = -bottomY;
let x = isMe(controller) ? rightX : leftX;
let y = isMe(controller) ? bottomY : topY;
if (zone === EXTRA) {
x = isMe(controller) ? leftX : rightX;
}
let rz = isMe(controller) ? 180 - DECK_ROTATE_Z.value : -DECK_ROTATE_Z.value;
if (zone === EXTRA) {
rz = isMe(controller) ? DECK_ROTATE_Z.value : DECK_ROTATE_Z.value;
}
const z = sequence;
api.start({
x,
y,
z,
rz,
zIndex: z,
height: DECK_CARD_HEIGHT.value,
});
};
import { isMe, type CardType } from "@/stores";
import { SpringApi } from "./types";
import { matConfig } from "../../utils";
import { ygopro } from "@/api";
import { easings } from "@react-spring/web";
import { asyncStart } from "./utils";
const {
PLANE_ROTATE_X,
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
CARD_RATIO,
COL_GAP,
ROW_GAP,
HAND_MARGIN_TOP,
HAND_CARD_HEIGHT,
HAND_CIRCLE_CENTER_OFFSET_Y,
DECK_OFFSET_X,
DECK_OFFSET_Y,
DECK_ROTATE_Z,
} = matConfig;
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
export const moveToField = async (props: {
card: CardType;
api: SpringApi;
report: boolean;
}) => {
const { card, api, report } = props;
// report
const { zone, sequence, controller, xyzMonster, position, overlayMaterials } =
card;
// 根据zone计算卡片的宽度
const cardWidth =
zone === SZONE
? BLOCK_HEIGHT_S.value * CARD_RATIO.value
: BLOCK_HEIGHT_M.value * CARD_RATIO.value;
const height = zone === SZONE ? BLOCK_HEIGHT_S.value : BLOCK_HEIGHT_M.value;
// 首先计算 x 和 y
let x = 0,
y = 0;
switch (zone) {
case SZONE: {
if (sequence === 5) {
// 场地魔法
x = -(
3 * (BLOCK_WIDTH.value + COL_GAP.value) -
(BLOCK_WIDTH.value - cardWidth) / 2
);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
y =
2 * (BLOCK_HEIGHT_M.value + ROW_GAP.value) -
(BLOCK_HEIGHT_M.value - BLOCK_HEIGHT_S.value) / 2;
}
break;
}
case MZONE: {
if (sequence > 4) {
// 额外怪兽区
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH.value + COL_GAP.value);
y = 0;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
}
break;
}
case OVERLAY: {
if (xyzMonster) {
const { sequence } = xyzMonster;
if (sequence > 4) {
// 额外怪兽区
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH.value + COL_GAP.value);
y = 0;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
}
}
break;
}
}
if (!isMe(controller)) {
x = -x;
y = -y;
}
await asyncStart(api)({
x,
y,
height,
z: overlayMaterials.length ? 200 : 120,
ry: [
ygopro.CardPosition.FACEDOWN,
ygopro.CardPosition.FACEDOWN_ATTACK,
ygopro.CardPosition.FACEDOWN_DEFENSE,
].includes(position ?? 5)
? 180
: 0,
rz: isMe(controller) ? 0 : 180,
config: {
// mass: 0.5,
easing: easings.easeOutSine,
},
});
await asyncStart(api)({
z: 0,
zIndex: overlayMaterials.length ? 3 : 1,
config: {
easing: easings.easeInSine,
mass: 5,
tension: 300, // 170
friction: 12, // 26
clamp: true,
},
});
};
import { isMe, type CardType, cardStore } from "@/stores";
import { SpringApi, ReportEnum } from "./types";
import { matConfig } from "../../utils";
import { ygopro } from "@/api";
import { easings } from "@react-spring/web";
import { asyncStart } from "./utils";
const {
PLANE_ROTATE_X,
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
CARD_RATIO,
COL_GAP,
ROW_GAP,
HAND_MARGIN_TOP,
HAND_CARD_HEIGHT,
HAND_CIRCLE_CENTER_OFFSET_Y,
DECK_OFFSET_X,
DECK_OFFSET_Y,
DECK_ROTATE_Z,
} = matConfig;
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
export const moveToHand = async (props: {
card: CardType;
api: SpringApi;
report: boolean;
}) => {
const { card, api, report } = props;
const { zone, sequence, controller } = card;
// 得刷新除了这个卡以外所有的自己的手卡
if (report) {
eventBus.emit(ReportEnum.ReloadHand, {
controller,
sequence,
});
}
// 手卡会有很复杂的计算...
const hand_circle_center_x = 0;
const hand_circle_center_y =
1 * BLOCK_HEIGHT_M.value +
1 * BLOCK_HEIGHT_S.value +
2 * ROW_GAP.value +
(HAND_MARGIN_TOP.value +
HAND_CARD_HEIGHT.value +
HAND_CIRCLE_CENTER_OFFSET_Y.value);
const hand_card_width = CARD_RATIO.value * HAND_CARD_HEIGHT.value;
const THETA =
2 *
Math.atan(
hand_card_width /
2 /
(HAND_CIRCLE_CENTER_OFFSET_Y.value + HAND_CARD_HEIGHT.value)
) *
0.9;
// 接下来计算每一张手卡
const hands_length = cardStore.at(HAND, controller).length;
const angle = (sequence - (hands_length - 1) / 2) * THETA;
const r = HAND_CIRCLE_CENTER_OFFSET_Y.value + HAND_CARD_HEIGHT.value / 2;
const negativeX = Math.sin(angle) * r;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2;
const x = hand_circle_center_x + negativeX * (isMe(controller) ? 1 : -1);
const y = hand_circle_center_y - negativeY + 130; // 常量 是手动调的 这里肯定有问题 有空来修
const _rz = (angle * 180) / Math.PI;
api.start({
x: isMe(controller) ? x : -x,
y: isMe(controller) ? y : -y,
rz: isMe(controller) ? _rz : 180 - _rz,
height: HAND_CARD_HEIGHT.value,
// rx: -PLANE_ROTATE_X.value,
});
};
import { isMe, type CardType } from "@/stores";
import { SpringApi } from "./types";
import { matConfig } from "../../utils";
import { ygopro } from "@/api";
import { easings } from "@react-spring/web";
import { asyncStart } from "./utils";
const {
PLANE_ROTATE_X,
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
CARD_RATIO,
COL_GAP,
ROW_GAP,
HAND_MARGIN_TOP,
HAND_CARD_HEIGHT,
HAND_CIRCLE_CENTER_OFFSET_Y,
DECK_OFFSET_X,
DECK_OFFSET_Y,
DECK_ROTATE_Z,
} = matConfig;
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
export const moveToOutside = async (props: {
card: CardType;
api: SpringApi;
report: boolean;
}) => {
const { card, api, report } = props;
// report
const { zone, sequence, controller, xyzMonster, position, overlayMaterials } =
card;
let x = 0,
y = 0;
if (zone === GRAVE) {
x = (BLOCK_WIDTH.value + COL_GAP.value) * 3;
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
} else if (zone === REMOVED) {
x = (BLOCK_WIDTH.value + COL_GAP.value) * 2;
}
if (!isMe(controller)) {
x = -x;
y = -y;
}
api.start({
x,
y,
z: 0,
rz: isMe(controller) ? 0 : 180,
});
};
import { type SpringValue, type SpringRef } from "@react-spring/web";
export type SpringApi = SpringRef<{
x: number;
y: number;
z: number;
rx: number;
ry: number;
rz: number;
zIndex: number;
height: number;
}>;
export enum ReportEnum {
ReloadHand = "reload-hand",
}
import { type SpringRef, type SpringConfig } from "@react-spring/web";
export const asyncStart = <T extends {}>(api: SpringRef<T>) => {
return (p: Partial<T> & { config: SpringConfig }) =>
new Promise((resolve) => {
api.start({
...p,
onRest: resolve,
});
});
};
section#mat {
margin-top: 200px;
// margin-top: 200px;
// padding-top: 50px; // 先不管 后面调整
position: relative;
#camera {
......@@ -8,12 +8,26 @@ section#mat {
flex-direction: column;
justify-content: center;
align-items: center;
perspective: var(--perspective);
// perspective: var(--perspective);
}
#plane {
transform: translateX(0) translateY(0) translateZ(0)
rotateX(var(--plane-rotate-x));
width: fit-content;
transform-style: preserve-3d;
perspective: var(--perspective);
}
}
.mat-card-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
transform-style: preserve-3d;
}
......@@ -22,9 +22,11 @@ export const Mat: FC = () => {
>
<Plane>
<Bg />
{snap.map((cardSnap, i) => (
<Card key={i} idx={i} />
))}
<CardContainer>
{snap.map((cardSnap, i) => (
<Card key={i} idx={i} />
))}
</CardContainer>
</Plane>
</section>
);
......@@ -35,3 +37,7 @@ const Plane: FC<PropsWithChildren> = ({ children }) => (
<div id="plane">{children}</div>
</div>
);
const CardContainer: FC<PropsWithChildren> = ({ children }) => (
<div className="mat-card-container">{children}</div>
);
......@@ -76,4 +76,8 @@ export const matConfig = {
value: 30,
unit: UNIT.DEG,
},
DECK_CARD_HEIGHT: {
value: 120,
unit: UNIT.PX,
},
};
import { cardStore } from "@/stores";
import { useSnapshot } from "valtio";
import { subscribeKey, watch } from "valtio/utils";
import { FC, memo, useEffect, useState } from "react";
import { ygopro } from "@/api";
import {
useSpring,
SpringValue,
animated,
useSpringRef,
} from "@react-spring/web";
export const Test = () => {
const snap = useSnapshot(cardStore.inner);
return (
<div
style={{
background: "white",
position: "fixed",
left: 0,
top: 0,
color: "black",
zIndex: 9999,
fontSize: 12,
}}
>
{snap.map((cardState, i) => (
<Card
idx={i}
key={i}
show={[
ygopro.CardZone.HAND,
ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
ygopro.CardZone.GRAVE,
].includes(cardState.zone)}
/>
))}
</div>
);
};
export const Card: FC<{ idx: number; show: boolean }> = memo(
({ idx, show }) => {
const snap = useSnapshot(cardStore.inner[idx]);
const api = useSpringRef();
const props = useSpring({
ref: api,
from: { x: 0 },
});
// subscribeKey(cardStore.inner[idx], "zone", (value) => {
// api.start({
// to: {
// x: value * 100,
// },
// });
// });
watch((get) => {
get(cardStore.inner[idx]);
const zone = get(cardStore.inner[idx]).zone;
api.start({
to: {
x: zone * 100,
},
});
});
return show ? (
<animated.div
style={{
transform: props.x.to((value) => `translateX(${value}px)`),
background: "white",
}}
>
<div>code: {snap.code}</div>
<div>{(Math.random() * 100).toFixed(0)}</div>
</animated.div>
) : (
<></>
);
},
(prev, next) => prev.show === next.show // 只有 show 变化时才会重新渲染
);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment