mirror of
https://github.com/unanmed/HumanBreak.git
synced 2026-11-08 13:44:30 +08:00
feat: 标题界面换用新渲染系统
This commit is contained in:
1 parent
58d2b5eb36
commit
f832d75f50
19 files changed
+849
-91
No files matched your search
@@ -501,3 +501,184 @@ export const StepForward = defineComponent<IconsProps>(props => {
|
||||
);
|
||||
};
|
||||
}, iconsProps);
|
||||
|
||||
export const SoundVolume = defineComponent<IconsProps>(props => {
|
||||
const path = ref<Path2D>();
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const generatePath = adjustPath(1, path, (ox, oy, width, height) => {
|
||||
const path = new Path2D();
|
||||
const left = ox + width / 8;
|
||||
const top = oy + height / 5;
|
||||
const bottom = oy + height - height / 5;
|
||||
path.moveTo(left, height / 2 - height / 10);
|
||||
path.lineTo(left, height / 2 + height / 10);
|
||||
path.lineTo(left + width / 6, height / 2 + height / 10);
|
||||
path.lineTo(width / 2, bottom);
|
||||
path.lineTo(width / 2, top);
|
||||
path.lineTo(left + width / 6, height / 2 - height / 10);
|
||||
path.closePath();
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const start = -Math.PI / 4;
|
||||
const end = Math.PI / 4;
|
||||
path.moveTo(
|
||||
width / 2 + (Math.SQRT1_2 * width) / 6,
|
||||
height / 2 - (Math.SQRT1_2 * width) / 6
|
||||
);
|
||||
path.arc(cx, cy, width / 6, start, end);
|
||||
path.moveTo(
|
||||
width / 2 + (Math.SQRT1_2 * width) / 3,
|
||||
height / 2 - (Math.SQRT1_2 * width) / 3
|
||||
);
|
||||
path.arc(cx, cy, width / 3, start, end);
|
||||
return path;
|
||||
});
|
||||
|
||||
watch(props, () => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<g-path
|
||||
loc={props.loc}
|
||||
path={path.value}
|
||||
stroke
|
||||
lineJoin="round"
|
||||
lineCap="round"
|
||||
></g-path>
|
||||
);
|
||||
};
|
||||
}, iconsProps);
|
||||
|
||||
export const Fullscreen = defineComponent<IconsProps>(props => {
|
||||
const path = ref<Path2D>();
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const generatePath = adjustPath(1, path, (ox, oy, width, height) => {
|
||||
const path = new Path2D();
|
||||
const left = ox + width / 4;
|
||||
const right = ox + width - width / 4;
|
||||
const top = oy + height / 4;
|
||||
const bottom = oy + height - height / 4;
|
||||
|
||||
// 左上
|
||||
path.moveTo(left + width / 6, top + height / 6);
|
||||
path.lineTo(left, top);
|
||||
path.moveTo(left, top + height / 8);
|
||||
path.lineTo(left, top);
|
||||
path.lineTo(left + width / 8, top);
|
||||
|
||||
// 右上
|
||||
path.moveTo(right - width / 6, top + height / 6);
|
||||
path.lineTo(right, top);
|
||||
path.moveTo(right, top + height / 8);
|
||||
path.lineTo(right, top);
|
||||
path.lineTo(right - width / 8, top);
|
||||
|
||||
// 左下
|
||||
path.moveTo(left + width / 6, bottom - height / 6);
|
||||
path.lineTo(left, bottom);
|
||||
path.moveTo(left, bottom - height / 8);
|
||||
path.lineTo(left, bottom);
|
||||
path.lineTo(left + width / 8, bottom);
|
||||
|
||||
// 右下
|
||||
path.moveTo(right - width / 6, bottom - height / 6);
|
||||
path.lineTo(right, bottom);
|
||||
path.moveTo(right, bottom - height / 8);
|
||||
path.lineTo(right, bottom);
|
||||
path.lineTo(right - width / 8, bottom);
|
||||
return path;
|
||||
});
|
||||
|
||||
watch(props, () => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<g-path
|
||||
loc={props.loc}
|
||||
path={path.value}
|
||||
stroke
|
||||
lineJoin="round"
|
||||
lineCap="round"
|
||||
></g-path>
|
||||
);
|
||||
};
|
||||
}, iconsProps);
|
||||
|
||||
export const ExitFullscreen = defineComponent<IconsProps>(props => {
|
||||
const path = ref<Path2D>();
|
||||
|
||||
const width = computed(() => props.loc[2] ?? 200);
|
||||
const height = computed(() => props.loc[3] ?? 200);
|
||||
const generatePath = adjustPath(1, path, (ox, oy, width, height) => {
|
||||
const path = new Path2D();
|
||||
const left = ox + width / 4;
|
||||
const right = ox + width - width / 4;
|
||||
const top = oy + height / 4;
|
||||
const bottom = oy + height - height / 4;
|
||||
|
||||
// 左上
|
||||
path.moveTo(left + width / 6, top + height / 6);
|
||||
path.lineTo(left, top);
|
||||
path.moveTo(left + width / 24, top + height / 6);
|
||||
path.lineTo(left + width / 6, top + height / 6);
|
||||
path.lineTo(left + width / 6, top + height / 24);
|
||||
|
||||
// 右上
|
||||
path.moveTo(right - width / 6, top + height / 6);
|
||||
path.lineTo(right, top);
|
||||
path.moveTo(right - width / 24, top + height / 6);
|
||||
path.lineTo(right - width / 6, top + height / 6);
|
||||
path.lineTo(right - width / 6, top + height / 24);
|
||||
|
||||
// 左下
|
||||
path.moveTo(left + width / 6, bottom - height / 6);
|
||||
path.lineTo(left, bottom);
|
||||
path.moveTo(left + width / 24, bottom - height / 6);
|
||||
path.lineTo(left + width / 6, bottom - height / 6);
|
||||
path.lineTo(left + width / 6, bottom - height / 24);
|
||||
|
||||
// 右下
|
||||
path.moveTo(right - width / 6, bottom - height / 6);
|
||||
path.lineTo(right, bottom);
|
||||
path.moveTo(right - width / 24, bottom - height / 6);
|
||||
path.lineTo(right - width / 6, bottom - height / 6);
|
||||
path.lineTo(right - width / 6, bottom - height / 24);
|
||||
return path;
|
||||
});
|
||||
|
||||
watch(props, () => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
generatePath(width.value, height.value);
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<g-path
|
||||
loc={props.loc}
|
||||
path={path.value}
|
||||
stroke
|
||||
lineJoin="round"
|
||||
lineCap="round"
|
||||
></g-path>
|
||||
);
|
||||
};
|
||||
}, iconsProps);
|
||||
@@ -121,7 +121,7 @@ export const Tip = defineComponent<TipProps>((props, { expose }) => {
|
||||
loc={rectLoc.value}
|
||||
circle={[props.corner ?? 4]}
|
||||
fill
|
||||
fillStyle="rgba(40,40,40,0.8)"
|
||||
fillStyle="rgba(0,0,0,0.8)"
|
||||
/>
|
||||
<icon
|
||||
hidden={!showIcon.value}
|
||||
|
||||
@@ -2,18 +2,27 @@ import { Shader, ShaderProgram } from '@motajs/render-core';
|
||||
|
||||
export abstract class EffectBase<T> {
|
||||
/** 当前使用的程序 */
|
||||
protected readonly program: ShaderProgram;
|
||||
protected program: ShaderProgram | null = null;
|
||||
/** 当前使用的着色器渲染元素 */
|
||||
protected shader: Shader | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly shader: Shader,
|
||||
public readonly options: T
|
||||
) {
|
||||
/**
|
||||
* 在一个着色器元素上创建效果
|
||||
* @param shader 着色器程序
|
||||
* @param options 本效果的配置信息
|
||||
*/
|
||||
create(shader: Shader, options: T) {
|
||||
const vs = this.getVertex(options);
|
||||
const fs = this.getFragment(options);
|
||||
const program = shader.createProgram(ShaderProgram, vs, fs);
|
||||
const program = shader.createProgram(ShaderProgram);
|
||||
program.vs(vs);
|
||||
program.fs(fs);
|
||||
program.requestCompile();
|
||||
|
||||
this.program = program;
|
||||
this.shader = shader;
|
||||
|
||||
this.initProgram(program, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,13 +48,14 @@ export abstract class EffectBase<T> {
|
||||
* 更新着色器渲染
|
||||
*/
|
||||
requestUpdate() {
|
||||
this.shader.update();
|
||||
this.shader?.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用此着色器
|
||||
*/
|
||||
use() {
|
||||
if (!this.program || !this.shader) return;
|
||||
this.shader.useProgram(this.program);
|
||||
}
|
||||
}
|
||||
@@ -29,29 +29,33 @@ export class Image3DEffect
|
||||
|
||||
protected getFragment(): string {
|
||||
return /* glsl */ `
|
||||
out vec4 color;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = texture2D(u_sampler, v_texCoord);
|
||||
color = texture(u_sampler, v_texCoord);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
initProgram(program: ShaderProgram): void {
|
||||
if (!this.shader) return;
|
||||
program.defineUniformMatrix(
|
||||
'u_imageTransform',
|
||||
this.shader.U_MATRIX_4x4
|
||||
);
|
||||
const shader = this.shader;
|
||||
const aspect = shader.width / shader.height;
|
||||
this.proj.perspective((Math.PI * 2) / 3, aspect, 0.01, 1000);
|
||||
this.proj.perspective(Math.PI / 2, 1, 0.01, 1000);
|
||||
this.view.lookAt([0, 0, 1], [0, 0, 0], [0, 1, 0]);
|
||||
this.model.bind(this);
|
||||
this.view.bind(this);
|
||||
this.proj.bind(this);
|
||||
this.updateTransform();
|
||||
}
|
||||
|
||||
updateTransform(): void {
|
||||
if (!this.shader || !this.program) return;
|
||||
const matrix = this.program.getMatrix('u_imageTransform');
|
||||
if (!matrix) return;
|
||||
const trans = this.model.multiply(this.view).multiply(this.proj);
|
||||
const trans = this.proj.multiply(this.view).multiply(this.model);
|
||||
matrix.set(false, trans.mat);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { createApp } from '@motajs/render';
|
||||
import { defineComponent } from 'vue';
|
||||
import { UIController } from '@motajs/system-ui';
|
||||
import { mainSceneUI } from './ui/main';
|
||||
import { MAIN_HEIGHT, MAIN_WIDTH } from './shared';
|
||||
import { hook } from '@user/data-base';
|
||||
import { loading } from '@user/data-base';
|
||||
import { createLoopMap } from './loopMap';
|
||||
import { createElements } from './elements';
|
||||
import { mainRenderer } from './renderer';
|
||||
import { createUI } from './ui';
|
||||
import { createAction } from './action';
|
||||
import { createLegacy } from './legacy';
|
||||
import { sceneController } from './scene';
|
||||
import { GameTitleUI } from './ui/title';
|
||||
|
||||
export function createGameRenderer() {
|
||||
const App = defineComponent(_props => {
|
||||
const ui = new UIController('root-ui');
|
||||
ui.open(mainSceneUI, {});
|
||||
|
||||
return () => (
|
||||
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
||||
{ui.render()}
|
||||
{sceneController.render()}
|
||||
</container>
|
||||
);
|
||||
});
|
||||
@@ -26,14 +23,6 @@ export function createGameRenderer() {
|
||||
mainRenderer.hide();
|
||||
createApp(App).mount(mainRenderer);
|
||||
|
||||
hook.on('reset', () => {
|
||||
mainRenderer.show();
|
||||
});
|
||||
|
||||
hook.on('restart', () => {
|
||||
mainRenderer.hide();
|
||||
});
|
||||
|
||||
console.log(mainRenderer);
|
||||
}
|
||||
|
||||
@@ -43,6 +32,11 @@ export function createRender() {
|
||||
createUI();
|
||||
createAction();
|
||||
createLoopMap();
|
||||
|
||||
loading.on('loaded', () => {
|
||||
sceneController.open(GameTitleUI, {});
|
||||
mainRenderer.show();
|
||||
});
|
||||
}
|
||||
|
||||
export * from './components';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { UIController } from '@motajs/system-ui';
|
||||
|
||||
export const sceneController = new UIController('main-scene');
|
||||
@@ -163,7 +163,7 @@ const MainScene = defineComponent(() => {
|
||||
}
|
||||
};
|
||||
|
||||
const loaded = ref(false);
|
||||
const loaded = ref(true);
|
||||
onLoaded(() => {
|
||||
loaded.value = true;
|
||||
});
|
||||
@@ -235,4 +235,4 @@ const MainScene = defineComponent(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export const mainSceneUI = new GameUI('main-scene', MainScene);
|
||||
export const MainSceneUI = new GameUI('main-scene', MainScene);
|
||||
@@ -0,0 +1,553 @@
|
||||
import { DefaultProps, onTick } from '@motajs/render-vue';
|
||||
import {
|
||||
GameUI,
|
||||
SetupComponentOptions,
|
||||
UIComponentProps
|
||||
} from '@motajs/system-ui';
|
||||
import { defineComponent, nextTick, onMounted, ref } from 'vue';
|
||||
import { MAIN_HEIGHT, MAIN_WIDTH } from '../shared';
|
||||
import {
|
||||
IActionEvent,
|
||||
MotaOffscreenCanvas2D,
|
||||
Shader,
|
||||
Sprite
|
||||
} from '@motajs/render-core';
|
||||
import { Image3DEffect } from '../fx';
|
||||
import {
|
||||
ITransitionedController,
|
||||
transitioned,
|
||||
transitionedColor,
|
||||
useKey
|
||||
} from '../use';
|
||||
import { hyper, linear, sleep } from 'mutate-animate';
|
||||
import { Font } from '@motajs/render-style';
|
||||
import { ExitFullscreen, Fullscreen, SoundVolume } from '../components';
|
||||
import { mainSetting, triggerFullscreen } from '@motajs/legacy-ui';
|
||||
import { saveLoad } from './save';
|
||||
import { MainSceneUI } from './main';
|
||||
|
||||
const enum TitleButton {
|
||||
StartGame,
|
||||
LoadGame,
|
||||
Replay,
|
||||
Achievement
|
||||
}
|
||||
|
||||
interface ButtonItem {
|
||||
code: TitleButton;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ButtonOption {
|
||||
code: number;
|
||||
color: string;
|
||||
name: string;
|
||||
hard: string;
|
||||
colorTrans: ITransitionedController<string>;
|
||||
scale: ITransitionedController<number>;
|
||||
}
|
||||
|
||||
export interface GameTitleProps extends DefaultProps, UIComponentProps {}
|
||||
|
||||
const gameTitleProps = {
|
||||
props: ['controller', 'instance']
|
||||
} satisfies SetupComponentOptions<GameTitleProps>;
|
||||
|
||||
export const GameTitle = defineComponent<GameTitleProps>(props => {
|
||||
const bg = core.material.images.images['bg.webp'];
|
||||
|
||||
//#region 计算背景图
|
||||
const aspect = bg.width / bg.height;
|
||||
const canvasAspect = MAIN_WIDTH / MAIN_HEIGHT;
|
||||
const [width, height] = (() => {
|
||||
if (canvasAspect > aspect) {
|
||||
const width = MAIN_WIDTH;
|
||||
const height = width / aspect;
|
||||
return [width, height];
|
||||
} else {
|
||||
const height = MAIN_HEIGHT;
|
||||
const width = height * aspect;
|
||||
return [width, height];
|
||||
}
|
||||
})();
|
||||
|
||||
//#region 标题设置
|
||||
|
||||
const fullscreen = ref(!!document.fullscreenElement);
|
||||
const soundOpened = ref(true);
|
||||
const selectHard = ref(false);
|
||||
|
||||
const buttonItems: ButtonItem[] = [
|
||||
{
|
||||
code: TitleButton.StartGame,
|
||||
color: 'rgb(40, 194, 255)',
|
||||
name: '开始游戏'
|
||||
},
|
||||
{
|
||||
code: TitleButton.LoadGame,
|
||||
color: 'rgb(0, 255, 55)',
|
||||
name: '读取存档'
|
||||
},
|
||||
{
|
||||
code: TitleButton.Replay,
|
||||
color: 'rgb(255, 251, 0)',
|
||||
name: '录像回放'
|
||||
},
|
||||
{
|
||||
code: TitleButton.Achievement,
|
||||
color: 'rgb(0, 208, 255)',
|
||||
name: '查看成就'
|
||||
}
|
||||
];
|
||||
|
||||
const buttons = buttonItems.map<ButtonOption>(v => {
|
||||
return {
|
||||
code: v.code,
|
||||
color: v.color,
|
||||
name: v.name,
|
||||
hard: '',
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
||||
};
|
||||
});
|
||||
|
||||
const hard = main.levelChoose.map<ButtonOption>(v => {
|
||||
return {
|
||||
code: v.hard,
|
||||
color: core.arrayToRGBA(v.color),
|
||||
name: v.title,
|
||||
hard: v.name,
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
||||
};
|
||||
});
|
||||
hard.push({
|
||||
code: main.levelChoose.length,
|
||||
color: '#aaa',
|
||||
name: '返回',
|
||||
hard: '',
|
||||
colorTrans: transitionedColor('#fff', 400, hyper('sin', 'out'))!,
|
||||
scale: transitioned(1, 400, hyper('sin', 'out'))!
|
||||
});
|
||||
|
||||
//#region 渐变动画
|
||||
const maskPos = transitioned(
|
||||
-MAIN_HEIGHT - 100,
|
||||
7000,
|
||||
hyper('sin', 'out')
|
||||
)!;
|
||||
const cursorX = transitioned(40, 400, hyper('sin', 'out'))!;
|
||||
const cursorY = transitioned(20, 400, hyper('sin', 'out'))!;
|
||||
|
||||
const soundColor = transitionedColor('#ddd', 400, hyper('sin', 'out'))!;
|
||||
|
||||
const buttonsAlpha = transitioned(1, 300, linear())!;
|
||||
const mainAlpha = transitioned(1, 600, linear())!;
|
||||
|
||||
const buttonFilter = `
|
||||
drop-shadow(3px 3px 5px rgba(0, 0, 0, 0.4))
|
||||
drop-shadow(0px 0px 2px rgba(255, 255, 255, 0.5))
|
||||
`;
|
||||
|
||||
let cursorScale = 1;
|
||||
|
||||
const titleFont = new Font('normal', 72).string();
|
||||
const buttonFont = new Font('normal', 24, 'px', 600);
|
||||
|
||||
//#region 按钮功能
|
||||
|
||||
const toggleHard = async () => {
|
||||
if (selectHard.value) {
|
||||
enterMain(0);
|
||||
} else {
|
||||
enterHard(0);
|
||||
}
|
||||
buttonsAlpha.set(0);
|
||||
await sleep(300);
|
||||
selectHard.value = !selectHard.value;
|
||||
buttonsAlpha.set(1);
|
||||
};
|
||||
|
||||
const loadGame = () => {
|
||||
saveLoad(props.controller, [0, 0, MAIN_WIDTH, MAIN_HEIGHT]);
|
||||
};
|
||||
|
||||
const replay = () => {
|
||||
core.chooseReplayFile();
|
||||
};
|
||||
|
||||
const startGame = async (hard: string) => {
|
||||
mainAlpha.set(0);
|
||||
await sleep(600);
|
||||
props.controller.close(props.instance);
|
||||
props.controller.open(MainSceneUI, {});
|
||||
nextTick(() => {
|
||||
core.startGame(hard);
|
||||
});
|
||||
};
|
||||
|
||||
const clickButton = (code: number) => {
|
||||
if (selectHard.value) {
|
||||
if (code === hard.length - 1) {
|
||||
toggleHard();
|
||||
return;
|
||||
}
|
||||
const item = hard[code];
|
||||
startGame(item.name);
|
||||
} else {
|
||||
switch (code) {
|
||||
case TitleButton.StartGame:
|
||||
toggleHard();
|
||||
break;
|
||||
case TitleButton.LoadGame:
|
||||
loadGame();
|
||||
break;
|
||||
case TitleButton.Replay:
|
||||
replay();
|
||||
break;
|
||||
case TitleButton.Achievement:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//#region 键盘操作
|
||||
|
||||
const selected = ref(0);
|
||||
|
||||
const [key] = useKey();
|
||||
key.realize(
|
||||
'@start_up',
|
||||
() => {
|
||||
selected.value--;
|
||||
if (selected.value < 0) {
|
||||
selected.value = 0;
|
||||
}
|
||||
if (selectHard.value) {
|
||||
enterHard(selected.value);
|
||||
} else {
|
||||
enterMain(selected.value);
|
||||
}
|
||||
},
|
||||
{ type: 'down' }
|
||||
)
|
||||
.realize(
|
||||
'@start_down',
|
||||
() => {
|
||||
selected.value++;
|
||||
if (selectHard.value) {
|
||||
if (selected.value > hard.length - 1) {
|
||||
selected.value = hard.length - 1;
|
||||
}
|
||||
enterHard(selected.value);
|
||||
} else {
|
||||
if (selected.value > buttons.length - 1) {
|
||||
selected.value = buttons.length - 1;
|
||||
}
|
||||
enterMain(selected.value);
|
||||
}
|
||||
},
|
||||
{ type: 'down' }
|
||||
)
|
||||
.realize('confirm', () => {
|
||||
clickButton(selected.value);
|
||||
});
|
||||
|
||||
//#region 鼠标操作
|
||||
|
||||
soundOpened.value = mainSetting.getValue('audio.bgmEnabled', true);
|
||||
if (soundOpened.value) {
|
||||
soundColor.set('#ddd');
|
||||
} else {
|
||||
soundColor.set('#d22');
|
||||
}
|
||||
|
||||
const moveCursor = (index: number) => {
|
||||
cursorX.set(40 - index * 10);
|
||||
cursorY.set(20 + 30 * index);
|
||||
};
|
||||
|
||||
const enterMain = (index: number) => {
|
||||
buttons.forEach((v, i) => {
|
||||
if (index !== i) {
|
||||
v.colorTrans.set('#fff');
|
||||
v.scale.set(1);
|
||||
} else {
|
||||
v.colorTrans.set(v.color);
|
||||
v.scale.set(1.1);
|
||||
}
|
||||
});
|
||||
selected.value = index;
|
||||
moveCursor(index);
|
||||
};
|
||||
|
||||
const enterHard = (index: number) => {
|
||||
hard.forEach((v, i) => {
|
||||
if (index !== i) {
|
||||
v.colorTrans.set('#fff');
|
||||
} else {
|
||||
v.colorTrans.set(v.color);
|
||||
}
|
||||
});
|
||||
selected.value = index;
|
||||
moveCursor(index);
|
||||
};
|
||||
|
||||
const toggleSound = () => {
|
||||
soundOpened.value = !soundOpened.value;
|
||||
mainSetting.setValue('audio.bgmEnabled', soundOpened.value);
|
||||
if (soundOpened.value) {
|
||||
soundColor.set('#ddd');
|
||||
} else {
|
||||
soundColor.set('#d22');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
await triggerFullscreen(!fullscreen.value);
|
||||
fullscreen.value = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
//#region 渲染
|
||||
|
||||
const imageEffect = new Image3DEffect();
|
||||
const imageShader = ref<Shader>();
|
||||
|
||||
const maskSprite = ref<Sprite>();
|
||||
const cursorSprite = ref<Sprite>();
|
||||
|
||||
let maskGradient: CanvasGradient | null = null;
|
||||
let titleGradient: CanvasGradient | null = null;
|
||||
|
||||
const createImageEffect = () => {
|
||||
if (!imageShader.value) return;
|
||||
imageEffect.create(imageShader.value);
|
||||
const model = imageEffect.getModel();
|
||||
const view = imageEffect.getView();
|
||||
view.lookAt([0, 0, 1], [0, 0, 0], [0, 1, 0]);
|
||||
model.scale(1.1, 1.1, 1.1);
|
||||
imageEffect.use();
|
||||
};
|
||||
|
||||
const createMaskGradient = (ctx: CanvasRenderingContext2D) => {
|
||||
maskGradient = ctx.createLinearGradient(100, 100, 200, 0);
|
||||
maskGradient.addColorStop(0, '#fff');
|
||||
maskGradient.addColorStop(1, '#000');
|
||||
};
|
||||
|
||||
const createTitleGradient = (ctx: CanvasRenderingContext2D) => {
|
||||
titleGradient = ctx.createLinearGradient(0, 0, 640, 0);
|
||||
titleGradient.addColorStop(0, 'rgb(0, 65, 62)');
|
||||
titleGradient.addColorStop(0.25, 'rgb(0, 33, 71)');
|
||||
titleGradient.addColorStop(0.5, 'rgb(136, 0, 214)');
|
||||
titleGradient.addColorStop(0.75, 'rgb(0, 2, 97)');
|
||||
titleGradient.addColorStop(1, 'rgb(0, 2, 97)');
|
||||
};
|
||||
|
||||
const titleSprite = ref<Sprite>();
|
||||
onMounted(() => {
|
||||
createImageEffect();
|
||||
maskPos.set(MAIN_WIDTH);
|
||||
enterMain(0);
|
||||
});
|
||||
|
||||
onTick(time => {
|
||||
if (maskPos.value < MAIN_WIDTH) {
|
||||
maskSprite.value?.update();
|
||||
}
|
||||
cursorScale = Math.sin(time / 600);
|
||||
cursorSprite.value?.update();
|
||||
});
|
||||
|
||||
const moveBackground = (ev: IActionEvent) => {
|
||||
const model = imageEffect.getModel();
|
||||
const px = (ev.offsetX / MAIN_WIDTH - 0.5) * 2;
|
||||
const py = (ev.offsetY / MAIN_HEIGHT - 0.5) * 2;
|
||||
model.reset();
|
||||
model.scale(1.1, 1.1, 1.1);
|
||||
model.rotateY(px / 24);
|
||||
model.rotateX(py / 24);
|
||||
};
|
||||
|
||||
const renderMask = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
if (maskGradient === null) {
|
||||
createMaskGradient(ctx);
|
||||
}
|
||||
const pos = maskPos.value;
|
||||
ctx.save();
|
||||
ctx.translate(pos, 0);
|
||||
ctx.fillStyle = 'transparent';
|
||||
ctx.fillStyle = maskGradient!;
|
||||
ctx.fillRect(0, 0, MAIN_WIDTH + MAIN_HEIGHT + 200, MAIN_HEIGHT);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const renderTitle = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
if (titleGradient === null) {
|
||||
createTitleGradient(ctx);
|
||||
}
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = titleFont;
|
||||
ctx.fillStyle = titleGradient!;
|
||||
ctx.filter = `
|
||||
drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.5))
|
||||
drop-shadow(-3px -3px 4px rgba(255,255,255,0.3))
|
||||
drop-shadow(12px 12px 4px rgba(0, 0, 0, 0.4))
|
||||
blur(1px)
|
||||
`;
|
||||
ctx.fillText(core.firstData.title, 320, 50);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const renderCursor = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const ctx = canvas.ctx;
|
||||
ctx.save();
|
||||
ctx.translate(0, 5);
|
||||
ctx.scale(1, cursorScale);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(1, -4);
|
||||
ctx.lineTo(9, 0);
|
||||
ctx.lineTo(1, 4);
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
return () => (
|
||||
<container
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
onMoveCapture={moveBackground}
|
||||
alpha={mainAlpha.ref.value}
|
||||
>
|
||||
<image
|
||||
image={bg}
|
||||
loc={[MAIN_WIDTH / 2, MAIN_HEIGHT / 2, width, height]}
|
||||
anc={[0.5, 0.5]}
|
||||
zIndex={0}
|
||||
/>
|
||||
<shader
|
||||
ref={imageShader}
|
||||
zIndex={5}
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
filter="brightness(120%)contrast(110%)"
|
||||
/>
|
||||
<sprite
|
||||
ref={maskSprite}
|
||||
render={renderMask}
|
||||
composite="multiply"
|
||||
loc={[0, 0, MAIN_WIDTH, MAIN_HEIGHT]}
|
||||
zIndex={20}
|
||||
noevent
|
||||
/>
|
||||
<sprite
|
||||
ref={titleSprite}
|
||||
render={renderTitle}
|
||||
loc={[MAIN_WIDTH / 2, 120, 640, 100, 0.5, 0.5]}
|
||||
zIndex={10}
|
||||
/>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[50, MAIN_HEIGHT, 200, 160]}
|
||||
anc={[0, 1]}
|
||||
>
|
||||
<container
|
||||
hidden={selectHard.value}
|
||||
loc={[0, 0, 200, 160]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
>
|
||||
{buttons.map((v, i) => {
|
||||
const x = 50 - i * 10;
|
||||
const y = 20 + i * 30;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
// 这个缩放性能影响极大,原因不明
|
||||
// scale={[v.scale.ref.value, v.scale.ref.value]}
|
||||
onEnter={() => enterMain(i)}
|
||||
onClick={() => clickButton(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
<container
|
||||
hidden={!selectHard.value}
|
||||
loc={[0, 0, 200, 160]}
|
||||
alpha={buttonsAlpha.ref.value}
|
||||
>
|
||||
{hard.map((v, i) => {
|
||||
const x = 50 - i * 10;
|
||||
const y = 20 + i * 30;
|
||||
return (
|
||||
<text
|
||||
text={v.name}
|
||||
font={buttonFont}
|
||||
loc={[x, y, void 0, void 0, 0, 0.5]}
|
||||
cursor="pointer"
|
||||
filter={buttonFilter}
|
||||
fillStyle={v.colorTrans.ref.value}
|
||||
onEnter={() => enterHard(i)}
|
||||
onClick={() => clickButton(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</container>
|
||||
<sprite
|
||||
ref={cursorSprite}
|
||||
width={10}
|
||||
height={10}
|
||||
render={renderCursor}
|
||||
loc={[cursorX.ref.value, cursorY.ref.value]}
|
||||
anc={[1, 0.5]}
|
||||
nocache
|
||||
/>
|
||||
</container>
|
||||
<container
|
||||
zIndex={15}
|
||||
loc={[MAIN_WIDTH - 40, MAIN_HEIGHT - 20, 80, 40, 1, 1]}
|
||||
>
|
||||
<SoundVolume
|
||||
loc={[0, 0, 40, 40]}
|
||||
cursor="pointer"
|
||||
strokeStyle={soundColor.ref.value}
|
||||
onClick={toggleSound}
|
||||
/>
|
||||
{!fullscreen.value ? (
|
||||
<Fullscreen
|
||||
loc={[40, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
) : (
|
||||
<ExitFullscreen
|
||||
loc={[40, 0, 40, 40]}
|
||||
onClick={toggleFullscreen}
|
||||
cursor="pointer"
|
||||
/>
|
||||
)}
|
||||
{!soundOpened.value && (
|
||||
<g-line
|
||||
line={[5, 35, 35, 5]}
|
||||
strokeStyle="gray"
|
||||
lineWidth={3}
|
||||
lineCap="round"
|
||||
noevent
|
||||
zIndex={5}
|
||||
/>
|
||||
)}
|
||||
</container>
|
||||
</container>
|
||||
);
|
||||
}, gameTitleProps);
|
||||
|
||||
export const GameTitleUI = new GameUI('game-title', GameTitle);
|
||||
Reference in new issue
Block a user