feat: 标题界面换用新渲染系统

This commit is contained in:
unanmed 2025-06-22 17:01:59 +08:00
parent 58d2b5eb36
commit f832d75f50
19 changed files with 849 additions and 91 deletions

View File

@ -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);

View File

@ -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}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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';

View File

@ -0,0 +1,3 @@
import { UIController } from '@motajs/system-ui';
export const sceneController = new UIController('main-scene');

View File

@ -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);

View File

@ -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);

View File

@ -31,7 +31,6 @@ import {
loadDefaultResource,
LoadTask
} from '@motajs/legacy-common';
import { GameUi } from '../controller';
import { formatSize } from '../utils';
import { logger } from '@motajs/common';
import { sleep } from 'mutate-animate';
@ -70,10 +69,10 @@ onMounted(async () => {
core._afterLoadResources(props.callback);
logger.log(`Resource load end.`);
loadDiv.style.opacity = '0';
await sleep(500);
Mota.require('@user/data-base').loading.emit('loaded');
await sleep(1000);
await sleep(500);
props.controller.close(props.num);
props.controller.open('start');
});
loadDiv = document.getElementById('load') as HTMLDivElement;
});

View File

@ -419,7 +419,8 @@ onUnmounted(() => {
);
background-clip: text;
-webkit-background-clip: text;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5),
text-shadow:
1px 1px 4px rgba(0, 0, 0, 0.5),
-1px -1px 3px rgba(255, 255, 255, 0.3),
5px 5px 5px rgba(0, 0, 0, 0.4);
filter: brightness(1.8);
@ -442,14 +443,17 @@ onUnmounted(() => {
position: absolute;
opacity: 0;
animation: cursor 2.5s linear 0s infinite normal running;
transition: left 0.4s ease-out, top 0.4s ease-out,
transition:
left 0.4s ease-out,
top 0.4s ease-out,
opacity 1.5s ease-out;
}
.start-button {
position: relative;
font: bold 1.5em 'normal';
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.4),
text-shadow:
1px 1px 2px rgba(0, 0, 0, 0.4),
0px 0px 1px rgba(255, 255, 255, 0.3);
background-clip: text;
-webkit-background-clip: text;

View File

@ -167,7 +167,7 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
protected framebufferMap: Map<string, WebGLFramebuffer> = new Map();
constructor(type: RenderItemPosition = 'static') {
super(type, !GL2.support);
super(type, false);
this.canvas = document.createElement('canvas');
this.gl = this.canvas.getContext('webgl2')!;
@ -216,17 +216,16 @@ export abstract class GL2<E extends EGL2Event = EGL2Event> extends RenderItem<
this.gl.useProgram(this.program.program);
}
if (this.cacheDirty) {
// 清空画布
const gl = this.gl;
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
this.drawScene(canvas, gl, this.program, transform);
this.cacheDirty = false;
}
// 清空画布
const gl = this.gl;
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
this.program.ready();
this.drawScene(canvas, gl, this.program, transform);
canvas.clear();
canvas.ctx.drawImage(this.canvas, 0, 0, this.width, this.height);
}
@ -1189,8 +1188,6 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
private shader: CompiledShader | null = null;
/** 当前的webgl程序 */
program: WebGLProgram | null = null;
/** 准备函数 */
private readyFn?: () => boolean;
/** 当前正在使用的顶点索引数组 */
usingIndices: IShaderIndices | null = null;
@ -1219,18 +1216,10 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
}
/**
* 使
* @param fn false
*/
setReady(fn: () => boolean) {
this.readyFn = fn;
}
/**
*
*
*/
ready(): boolean {
return this.readyFn?.() ?? true;
return true;
}
/**
@ -1255,7 +1244,6 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
case RenderMode.ElementsInstanced:
return this.elementsInstancedParams as DrawParamsMap[T];
}
return null;
}
/**

View File

@ -1,4 +1,4 @@
import { isNil } from 'lodash-es';
import { isEqual, isNil } from 'lodash-es';
import { EventEmitter } from 'eventemitter3';
import { MotaOffscreenCanvas2D } from './canvas2d';
import { Ticker, TickerFn } from 'mutate-animate';
@ -445,9 +445,9 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
canvas.ctx.drawImage(this.cache.canvas, ax, ay, width, height);
} else {
this.cacheDirty = false;
canvas.ctx.translate(ax, ay);
this.render(canvas, tran);
this.cacheDirty = false;
}
ctx.restore();
this.emit('afterRender', transform);
@ -505,6 +505,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
*
*/
size(width: number, height: number): void {
if (width === this.width && height === this.height) return;
this.width = width;
this.height = height;
if (this.enableCache) {
@ -1186,7 +1187,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
}
case 'filter': {
if (!this.assertType(nextValue, 'string', key)) return;
this.setFilter(this.filter);
this.setFilter(nextValue);
return;
}
case 'hd': {
@ -1238,6 +1239,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return;
}
case 'loc': {
if (isEqual(nextValue, prevValue)) return;
if (!this.assertType(nextValue, Array, key)) return;
if (!isNil(nextValue[0]) && !isNil(nextValue[1])) {
this.pos(nextValue[0] as number, nextValue[1] as number);
@ -1254,6 +1256,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return;
}
case 'anc': {
if (isEqual(nextValue, prevValue)) return;
if (!this.assertType(nextValue, Array, key)) return;
this.setAnchor(nextValue[0] as number, nextValue[1] as number);
return;
@ -1264,6 +1267,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
return;
}
case 'scale': {
if (isEqual(nextValue, prevValue)) return;
if (!this.assertType(nextValue, Array, key)) return;
this._transform.setScale(
nextValue[0] as number,

View File

@ -26,8 +26,10 @@ void main() {
}
`;
const DEFAULT_FS = /* glsl */ `
out vec4 color;
void main() {
gl_FragColor = texture2D(u_sampler, v_texCoord);
color = texture(u_sampler, v_texCoord);
}
`;
@ -55,7 +57,7 @@ export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
}
export class ShaderProgram extends GL2Program {
protected override readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
protected readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
constructor(gl2: GL2, vs?: string, fs?: string) {
super(gl2, vs, fs);
@ -66,6 +68,11 @@ export class ShaderProgram extends GL2Program {
}
}
ready(): boolean {
this.useIndices('defaultIndices');
return true;
}
protected override compile() {
const success = super.compile();
if (!success) return false;
@ -76,7 +83,7 @@ export class ShaderProgram extends GL2Program {
const tex = this.defineAttribArray('a_texCoord');
const position = this.defineAttribArray('a_position');
const sampler = this.defineTexture('u_sampler', 0);
const indices = this.defineIndices('defalutIndices');
const indices = this.defineIndices('defaultIndices');
if (!tex || !position || !sampler || !indices) {
return true;
}
@ -91,6 +98,7 @@ export class ShaderProgram extends GL2Program {
tex.enable();
indices.buffer(new Uint16Array([0, 1, 2, 2, 3, 1]), gl.STATIC_DRAW);
this.useIndices(indices);
this.mode(this.element.DRAW_ELEMENTS);
this.paramElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
return true;

View File

@ -5,7 +5,7 @@ import {
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { logger } from '@motajs/common';
import { clamp, isNil } from 'lodash-es';
import { clamp, isEqual, isNil } from 'lodash-es';
import { CanvasStyle } from './types';
export type CircleParams = [
@ -442,6 +442,7 @@ export class Circle extends GraphicItemBase {
this.setAngle(this.start, nextValue);
return true;
case 'circle': {
if (isEqual(nextValue, prevValue)) return true;
const value = nextValue as CircleParams;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radius, start, end] = value;
@ -531,6 +532,7 @@ export class Ellipse extends GraphicItemBase {
this.setAngle(this.start, nextValue);
return true;
case 'ellipse': {
if (isEqual(nextValue, prevValue)) return true;
const value = nextValue as EllipseParams;
if (!this.assertType(value, Array, key)) return false;
const [cx, cy, radiusX, radiusY, start, end] = value;
@ -619,6 +621,7 @@ export class Line extends GraphicItemBase {
this.setPoint2(this.x2, nextValue);
return true;
case 'line':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
@ -750,6 +753,7 @@ export class BezierCurve extends GraphicItemBase {
this.setEnd(this.ex, nextValue);
return true;
case 'curve':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}
@ -873,6 +877,7 @@ export class QuadraticCurve extends GraphicItemBase {
this.setEnd(this.ex, nextValue);
return true;
case 'curve':
if (isEqual(nextValue, prevValue)) return true;
if (!this.assertType(nextValue as number[], Array, key)) {
return false;
}

View File

@ -98,6 +98,8 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
/** 当前正在按下的按键 */
private pressed: Set<KeyCode> = new Set();
/** 有哪些单次按下的按键已经触发 */
private downEmitted: Set<KeyCode> = new Set();
/** 按键按下时的时间 */
private pressTime: Map<KeyCode, number> = new Map();
/** 按键节流时间 */
@ -262,6 +264,10 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
});
this.emit('emit', key, assist, type);
if (type === 'down') {
this.downEmitted.add(key);
}
return emitted;
}
@ -284,6 +290,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
if (!this.pressed.has(keyCode)) return;
this.pressed.delete(keyCode);
this.pressTime.delete(keyCode);
this.downEmitted.delete(keyCode);
this.emit('release', keyCode);
}
@ -306,7 +313,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
}
if (!config) return false;
// 按下单次触发
if (config.type === 'down') return !this.pressed.has(keyCode);
if (config.type === 'down') return !this.downEmitted.has(keyCode);
// 按下重复触发
if (config.type === 'down-repeat') return true;
if (config.type === 'down-timeout') {

View File

@ -247,9 +247,6 @@ control.prototype.showStartAnimate = function (noAnimate, callback) {
core.flags.startUsingCanvas,
callback
);
Mota.r(() => {
Mota.require('@motajs/legacy-ui').fixedUi.open('start');
});
};
control.prototype._showStartAnimate_resetDom = function () {
@ -526,17 +523,20 @@ control.prototype.setHeroMoveInterval = function (callback) {
// render.move(true);
// });
core.interval.heroMoveInterval = window.setInterval(function () {
// render.offset += toAdd * 4;
core.status.heroMoving += toAdd;
if (core.status.heroMoving >= 8) {
clearInterval(core.interval.heroMoveInterval);
core.status.heroMoving = 0;
// render.offset = 0;
// render.move(false);
if (callback) callback();
}
}, ((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed);
core.interval.heroMoveInterval = window.setInterval(
function () {
// render.offset += toAdd * 4;
core.status.heroMoving += toAdd;
if (core.status.heroMoving >= 8) {
clearInterval(core.interval.heroMoveInterval);
core.status.heroMoving = 0;
// render.offset = 0;
// render.move(false);
if (callback) callback();
}
},
((core.values.moveSpeed / 8) * toAdd) / core.status.replay.speed
);
};
////// 每移动一格后执行的事件 //////

View File

@ -24,9 +24,6 @@ events.prototype.resetGame = function (hero, hard, floorId, maps, values) {
////// 游戏开始事件 //////
events.prototype.startGame = function (hard, seed, route, callback) {
hard = hard || '';
if (!main.replayChecking) {
Mota.require('@motajs/legacy-ui').fixedUi.closeByName('start');
}
if (main.mode != 'play') return;
Mota.require('@user/data-state').resetSkillLevel();
@ -701,12 +698,12 @@ events.prototype.getItem = function (id, num, x, y, isGentleClick, callback) {
(id.endsWith('Key')
? '(钥匙类道具,遇到对应的门时自动打开)'
: itemCls == 'tools'
? '消耗类道具请按T在道具栏使用'
: itemCls == 'constants'
? '永久类道具请按T在道具栏使用'
: itemCls == 'equips'
? '装备类道具请按Q在装备栏进行装备'
: '')
? '消耗类道具请按T在道具栏使用'
: itemCls == 'constants'
? '永久类道具请按T在道具栏使用'
: itemCls == 'equips'
? '装备类道具请按Q在装备栏进行装备'
: '')
);
}
itemHint.push(id);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -31,6 +31,7 @@ body {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
display: none;
}
#curtain {