Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/webgal/public/game/scene/demo_zh_cn.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ setTransition: -target=bg-main -exit=shockwaveOut;
:你好|欢迎来到 {engine} 的世界;
changeBg:bg.webp -next;
setTransition: -target=bg-main -enter=shockwaveIn -next;
unlockCg:bg.webp -name=良い夜; // 解锁CG并赋予名称
unlockCg:bg.webp -name=良い夜 -series=op; // 解锁CG并赋予名称
unlockCg:WebGAL_New_Enter_Image.webp -name=Enter -series=op; // 解锁CG并赋予名称
changeFigure:stand.webp -left -enter=enter-from-left -animationFlag=on -eyesOpen=stand.webp -eyesClose=stand.webp -mouthOpen=open_mouth.webp -mouthHalfOpen=open_mouth.webp -mouthClose=stand.webp -next;
miniAvatar:miniavatar.webp;
{heroine}:欢迎使用 {engine}!这是一款全新的网页端视觉小说引擎。 -v1.wav -left;
Expand Down
3 changes: 2 additions & 1 deletion packages/webgal/src/Core/gameScripts/changeBg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const changeBg = (sentence: ISentence): IPerform => {
const url = sentence.content;
const unlockName = getStringArgByKey(sentence, 'unlockname') ?? '';
const series = getStringArgByKey(sentence, 'series') ?? 'default';
const order = getNumberArgByKey(sentence, 'order') ?? 0;
const transformString = getStringArgByKey(sentence, 'transform');
let duration = getNumberArgByKey(sentence, 'duration') ?? DEFAULT_BG_OUT_DURATION;
const enterDuration = getNumberArgByKey(sentence, 'enterDuration') ?? duration;
Expand All @@ -34,7 +35,7 @@ export const changeBg = (sentence: ISentence): IPerform => {

const dispatch = webgalStore.dispatch;
if (unlockName !== '') {
dispatch(unlockCgInUserData({ name: unlockName, url, series }));
dispatch(unlockCgInUserData({ name: unlockName, url, series, order }));
const userDataState = webgalStore.getState().userData;
localforage.setItem(WebGAL.gameKey, userDataState).then(() => {});
}
Expand Down
7 changes: 4 additions & 3 deletions packages/webgal/src/Core/gameScripts/unlockCg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { logger } from '@/Core/util/logger';
import localforage from 'localforage';

import { WebGAL } from '@/Core/WebGAL';
import { getStringArgByKey } from '../util/getSentenceArg';
import { getStringArgByKey, getNumberArgByKey } from '../util/getSentenceArg';

/**
* 解锁cg
Expand All @@ -16,8 +16,9 @@ export const unlockCg = (sentence: ISentence): IPerform => {
const url = sentence.content;
const name = getStringArgByKey(sentence, 'name') ?? sentence.content;
const series = getStringArgByKey(sentence, 'series') ?? 'default';
logger.info(`解锁CG:${name},路径:${url},所属系列:${series}`);
webgalStore.dispatch(unlockCgInUserData({ name, url, series }));
const order = getNumberArgByKey(sentence, 'order') ?? 0;
logger.info(`解锁CG:${name},路径:${url},所属系列:${series},排序:${order}`);
webgalStore.dispatch(unlockCgInUserData({ name, url, series, order }));
const userDataState = webgalStore.getState().userData;
localforage.setItem(WebGAL.gameKey, userDataState).then(() => {});
return createNonePerform();
Expand Down
62 changes: 54 additions & 8 deletions packages/webgal/src/UI/Extra/ExtraCg.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import styles from '@/UI/Extra/extra.module.scss';
import React from 'react';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import { useValue } from '@/hooks/useValue';
import './extraCG_animation_List.scss';
import { ExtraCgElement } from '@/UI/Extra/ExtraCgElement';
import { ExtraCgElement, isVideoFile } from '@/UI/Extra/ExtraCgElement';
import useSoundEffect from '@/hooks/useSoundEffect';
import { IAppreciationAsset } from '@/store/userDataInterface';

interface IExtraCgDisplayItem {
key: string;
name: string;
resources: IAppreciationAsset[];
}

export function ExtraCg() {
const cgPerPage = 8;
const extraState = useSelector((state: RootState) => state.userData.appreciationData);
const pageNumber = Math.ceil(extraState.cg.length / cgPerPage);
// const pageNumber = 10;
const groupedCgList = useMemo(() => buildGroupedCgList(extraState.cg), [extraState.cg]);
const pageNumber = Math.ceil(groupedCgList.length / cgPerPage);
const currentPage = useValue(1);
const { playSeEnter, playSeClick } = useSoundEffect();

// 开始生成立绘鉴赏的图片
const showCgList = [];
const len = extraState.cg.length;
const len = groupedCgList.length;
for (
let i = (currentPage.value - 1) * cgPerPage;
i < Math.min(len, (currentPage.value - 1) * cgPerPage + cgPerPage);
Expand All @@ -27,11 +34,11 @@ export function ExtraCg() {
const deg = Random(-5, 5);
const temp = (
<ExtraCgElement
name={extraState.cg[i].name}
resourceUrl={extraState.cg[i].url}
name={groupedCgList[i].name}
resources={groupedCgList[i].resources}
transformDeg={deg}
index={index}
key={index.toString() + extraState.cg[i].url}
key={groupedCgList[i].key}
/>
);
showCgList.push(temp);
Expand Down Expand Up @@ -73,3 +80,42 @@ export function ExtraCg() {
function Random(min: number, max: number) {
return Math.round(Math.random() * (max - min)) + min;
}

function buildGroupedCgList(cgList: IAppreciationAsset[]): IExtraCgDisplayItem[] {
const groupedCgList: IExtraCgDisplayItem[] = [];
const seriesIndexMap = new Map<string, number>();

cgList.forEach((cg, index) => {
// 视频不进行分组
if (cg.series !== 'default' && !isVideoFile(cg.url)) {
const groupedIndex = seriesIndexMap.get(cg.series);
if (groupedIndex !== undefined) {
groupedCgList[groupedIndex].resources.push(cg);
return;
}

seriesIndexMap.set(cg.series, groupedCgList.length);
groupedCgList.push({
key: `series:${cg.series}`,
name: cg.name,
resources: [cg],
});
return;
}

groupedCgList.push({
key: `default:${index}:${cg.url}`,
name: cg.name,
resources: [cg],
});
});

return groupedCgList.map((groupedCg) => ({
...groupedCg,
resources: groupedCg.resources.length > 1 ? [...groupedCg.resources].sort(sortCgByOrder) : groupedCg.resources,
}));
}

function sortCgByOrder(prev: IAppreciationAsset, next: IAppreciationAsset) {
return (prev.order ?? 0) - (next.order ?? 0);
}
97 changes: 74 additions & 23 deletions packages/webgal/src/UI/Extra/ExtraCgElement.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
import { useValue } from '@/hooks/useValue';
import styles from '@/UI/Extra/extra.module.scss';
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import useSoundEffect from '@/hooks/useSoundEffect';
import { IAppreciationAsset } from '@/store/userDataInterface';

export const isVideoFile = (url: string) => {
const extension = url.split('.').pop()?.toLowerCase() || '';
return ['mp4', 'webm', 'mkv'].includes(extension);
}

interface IProps {
name: string;
resourceUrl: string;
resources: IAppreciationAsset[];
transformDeg: number;
index: number;
}

export function ExtraCgElement(props: IProps) {
const showFull = useValue(false);
const [currentResourceIndex, setCurrentResourceIndex] = useState(0);
const { playSeEnter, playSeClick } = useSoundEffect();
const previewResource = props.resources[0];
const currentResource = props.resources[currentResourceIndex] ?? previewResource;

// Determine if the resource is a video based on file extension
const isVideo = useMemo(() => {
const extension = props.resourceUrl.split('.').pop()?.toLowerCase() || '';
return ['mp4', 'webm', 'mkv'].includes(extension);
}, [props.resourceUrl]);
return isVideoFile(previewResource.url);
}, [previewResource.url]);

// Determine if the resource is an image based on file extension
const isImage = useMemo(() => {
const extension = props.resourceUrl.split('.').pop()?.toLowerCase() || '';
const extension = previewResource.url.split('.').pop()?.toLowerCase() || '';
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'].includes(extension);
}, [props.resourceUrl]);
}, [previewResource.url]);
Comment on lines 32 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

isImage 的 memo 检查是多余的,因为在 renderMedia 函数中,isImage 分支和 else 分支(回退逻辑)的代码逻辑完全相同。删除它可以简化组件逻辑并减少不必要的计算。

  // 移除冗余的 isImage 检查

const isStackPreview = props.resources.length > 1 && isImage;
const animationDelay = 100 + props.index * 100;
const stackResources = isStackPreview ? [...props.resources].reverse() : [];

const getStackItemStyle = (index: number, length: number) => {
const offset = index - (length - 1) / 2;
return {
zIndex: index,
animationDelay: `${animationDelay + index * 140}ms`,
'--cg-stack-start-transform': `translate(${offset * 1.5}%, ${offset * -0.8}%) rotate(${offset * 2}deg)`,
'--cg-stack-end-transform': `translate(${offset * 6}%, ${offset * -3}%) rotate(${offset * 6}deg)`,
} as React.CSSProperties;
};

// Render media content based on resource type
const renderMedia = (fullScreen: boolean) => {
const renderMedia = (resourceUrl: string) => {
if (isVideo) {
return (
<video
src={props.resourceUrl}
src={resourceUrl}
autoPlay
loop
muted
Expand All @@ -47,7 +68,7 @@ export function ExtraCgElement(props: IProps) {
return (
<div
style={{
backgroundImage: `url('${props.resourceUrl}')`,
backgroundImage: `url('${resourceUrl}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100%',
Expand All @@ -60,7 +81,7 @@ export function ExtraCgElement(props: IProps) {
return (
<div
style={{
backgroundImage: `url('${props.resourceUrl}')`,
backgroundImage: `url('${resourceUrl}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100%',
Expand All @@ -71,33 +92,63 @@ export function ExtraCgElement(props: IProps) {
}
};

const openFullPreview = () => {
setCurrentResourceIndex(0);
showFull.set(true);
playSeClick();
};

const closeFullPreview = () => {
setCurrentResourceIndex(0);
showFull.set(false);
playSeClick();
};

const handleFullPreviewClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();

if (currentResourceIndex >= props.resources.length - 1) {
closeFullPreview();
return;
}

playSeClick();
setCurrentResourceIndex((currentIndex) => currentIndex + 1);
};

return (
<>
{showFull.value && (
<div
onClick={() => {
showFull.set(!showFull.value);
playSeClick();
}}
onClick={closeFullPreview}
className={styles.showFullContainer}
onMouseEnter={playSeEnter}
>
<div className={styles.showFullCgMain}>{renderMedia(true)}</div>
<div className={styles.showFullCgMain} onClick={(e) => handleFullPreviewClick(e)}>
{renderMedia(currentResource.url)}
</div>
</div>
)}
<div
onClick={() => {
showFull.set(!showFull.value);
playSeClick();
}}
onClick={openFullPreview}
onMouseEnter={playSeEnter}
style={{
animation: `cg_softIn_${props.transformDeg} 1.5s ease-out ${100 + props.index * 100}ms forwards`,
animation: `cg_softIn_${isStackPreview ? 0 : props.transformDeg} 1.5s ease-out ${animationDelay}ms forwards`,
}}
key={props.name}
className={styles.cgElement}
className={`${styles.cgElement} ${isStackPreview ? styles.cgElementStack : ''}`}
>
{renderMedia(false)}
{isStackPreview ? (
<>
{stackResources.map((resource, index) => (
<div className={styles.cgStackItem} style={getStackItemStyle(index, stackResources.length)} key={`${resource.url}_${index}`}>
{renderMedia(resource.url)}
</div>
))}
</>
) : (
renderMedia(previewResource.url)
)}
</div>
</>
);
Expand Down
31 changes: 31 additions & 0 deletions packages/webgal/src/UI/Extra/extra.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,37 @@
cursor: pointer;
}

.cgElementStack {
background-color: transparent;
box-shadow: none;
}

.cgStackItem {
position: absolute;
inset: 0.6em;
box-sizing: border-box;
padding: 0.75em;
background-color: rgba(255, 255, 255, 0.85);
box-shadow: 0 0 15px 5px rgba(0, 0, 0, 0.28);
transform-origin: center 70%;
opacity: 0;
animation: cgStackItemIn 1.5s ease-out both;
}

@keyframes cgStackItemIn {
0% {
opacity: 0;
transform: var(--cg-stack-start-transform);
}
20% {
opacity: 1;
}
100% {
opacity: 1;
transform: var(--cg-stack-end-transform);
}
}

.cgShowDiv {
height: 8%;
width: 100%;
Expand Down
Loading
Loading