HumanBreak/src/ui/danmakuEditor.vue
2024-04-23 21:43:26 +08:00

625 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="danmaku-editor" @click.stop>
<div id="danmaku-input">
<span
class="danmaku-tool"
:open="cssOpened"
@click="openTool('css')"
>css</span
>
<font-colors-outlined
class="danmaku-tool"
:open="fillOpened"
@click="openTool('fillColor')"
/>
<highlight-outlined
class="danmaku-tool"
:open="strokeOpened"
@click="openTool('strokeColor')"
/>
<meh-outlined
class="danmaku-tool"
:open="iconOpened"
@click="openTool('icon')"
/>
<div id="danmaku-input-div">
<a-input
id="danmaku-input-input"
:max-length="200"
v-model:value="inputValue"
placeholder="请在此输入弹幕"
autocomplete="off"
@change="input(inputValue)"
@pressEnter="inputEnter()"
/>
</div>
<send-outlined
class="danmaku-tool danmaku-post"
:posting="posting"
@click="send()"
/>
</div>
<Transition name="danmaku">
<div v-if="cssOpened" id="danmaku-css">
<span id="danmaku-css-hint">编辑弹幕的 CSS 样式</span>
<a-input
id="danmaku-css-input"
:max-length="300"
v-model:value="cssInfo"
placeholder="请在此输入样式"
autocomplete="off"
@blur="inputCSS(cssInfo)"
@pressEnter="inputCSS(cssInfo)"
/>
<span v-if="cssError" id="danmaku-css-error">{{
cssError
}}</span>
</div>
<div v-else-if="iconOpened" id="danmaku-icon">
<span id="danmaku-icon-hint">常用图标</span>
<Scroll
class="danmaku-icon-scroll"
:no-scroll="true"
type="horizontal"
>
<div id="danmaku-icon-div">
<span
class="danmaku-icon-one"
v-for="icon of frequentlyIcon"
@click="addIcon(icon as AllIds)"
>
<BoxAnimate
:id="(icon as AllIds)"
:noborder="true"
:no-animate="true"
:height="getIconHeight(icon as AllIds)"
></BoxAnimate>
</span>
</div>
</Scroll>
<span
id="danmaku-icon-all"
class="button-text"
:active="iconAll"
@click="iconAll = !iconAll"
>
所有图标 <up-outlined />
</span>
</div>
<div v-else-if="fillOpened || strokeOpened" id="danmaku-color">
<span id="danmaku-color-hint">设置颜色</span>
<Scroll
class="danmaku-color-scroll"
:no-scroll="true"
type="horizontal"
>
<div id="danmaku-color-container">
<span
v-for="color of frequentlyColor"
:style="{ backgroundColor: color }"
:selected="color === nowColor"
@click="inputColor(color)"
class="danmaku-color-one"
></span>
</div>
</Scroll>
<a-input
id="danmaku-color-input"
:max-length="100"
v-model:value="nowColor"
placeholder="输入颜色"
autocomplete="off"
@blur="inputColor(nowColor)"
@pressEnter="inputColor(nowColor)"
></a-input>
</div>
</Transition>
<Transition name="danmaku-icon">
<div v-if="iconAll" id="danmaku-icon-all-div">
<span
>本列表不包含额外素材如果需要额外素材请手动填写素材id</span
>
<Scroll class="danmaku-all-scroll">
<div id="danmaku-all-container">
<span
v-for="icon of getAllIcons()"
@click="addIcon(icon)"
>
<BoxAnimate
:id="icon"
:height="getIconHeight(icon)"
:no-animate="true"
:noborder="true"
></BoxAnimate>
</span>
</div>
</Scroll>
</div>
</Transition>
</div>
</template>
<script lang="ts" setup>
import { Ref, onMounted, onUnmounted, ref } from 'vue';
import {
FontColorsOutlined,
HighlightOutlined,
MehOutlined,
SendOutlined,
UpOutlined
} from '@ant-design/icons-vue';
import { Danmaku } from '@/core/main/custom/danmaku';
import { GameUi } from '@/core/main/custom/ui';
import { sleep } from 'mutate-animate';
import { fixedUi } from '@/core/main/init/ui';
import { tip } from '@/plugin/utils';
import { gameKey } from '@/core/main/init/hotkey';
import { isNil } from 'lodash-es';
import { stringifyCSS, parseCss, getIconHeight } from '@/plugin/utils';
import { logger, LogLevel } from '@/core/common/logger';
import Scroll from '@/components/scroll.vue';
import BoxAnimate from '@/components/boxAnimate.vue';
const props = defineProps<{
num: number;
ui: GameUi;
}>();
const frequentlyIcon: (AllIds | 'hero' | `X${number}`)[] = [
'hero',
'yellowKey',
'blueKey',
'redKey',
'A492',
'A494',
'A497',
'redPotion',
'redGem',
'blueGem',
'I559',
'X10194',
'downPortal',
'leftPortal',
'upPortal',
'rightPortal',
'upFloor',
'downFloor',
'greenSlime',
'yellowKnight',
'bat',
'slimelord'
];
const frequentlyColor: string[] = [
'#ffffff',
'#000000',
'#ff0000',
'#00ff00',
'#0000ff',
'#ffff00',
'#00ffff',
'#ff00ff',
'#c0c0c0',
'#808080',
'#800000',
'#800080',
'#008000',
'#808000',
'#000080',
'#008080'
];
let mainDiv: HTMLDivElement;
let danmaku = Danmaku.lastEditoredDanmaku ?? new Danmaku();
const cssOpened = ref(false);
const iconOpened = ref(false);
const fillOpened = ref(false);
const strokeOpened = ref(false);
const posting = ref(false);
const iconAll = ref(false);
const nowColor = ref('#ffffff');
const inputValue = ref(danmaku.text);
const cssInfo = ref(stringifyCSS(danmaku.style));
const cssError = ref('');
const map: Record<string, Ref<boolean>> = {
css: cssOpened,
icon: iconOpened,
fillColor: fillOpened,
strokeColor: strokeOpened
};
function openTool(tool: string) {
iconAll.value = false;
for (const [key, value] of Object.entries(map)) {
if (key === tool) {
value.value = !value.value;
} else {
value.value = false;
}
}
if (tool === 'fillColor') {
nowColor.value = danmaku.textColor;
} else if (tool === 'strokeColor') {
nowColor.value = danmaku.strokeColor;
}
}
function send() {
if (posting.value) return;
if (danmaku.text === '') {
tip('warning', '请填写弹幕!');
return;
}
if (!core.isPlaying()) {
tip('warning', '请进入游戏后再发送弹幕');
return;
}
const { x, y } = core.status.hero.loc;
const floor = core.status.floorId;
if (isNil(x) || isNil(y) || isNil(floor)) {
tip('warning', '当前无法发送弹幕');
return;
}
danmaku.x = x;
danmaku.y = y;
danmaku.floor = floor;
danmaku
.post()
.then(value => {
if (value.data.code === 0) {
danmaku.show();
danmaku = new Danmaku();
inputValue.value = '';
cssInfo.value = '';
}
})
.finally(() => {
posting.value = false;
});
}
function close() {
mainDiv.classList.remove('danmaku-startup');
mainDiv.classList.add('danmaku-close');
sleep(400).then(() => {
fixedUi.close(props.num);
});
}
function input(value: string) {
danmaku.text = value;
}
function inputEnter() {
input(inputValue.value);
inputCSS(cssInfo.value);
send();
}
function inputCSS(text: string) {
const { info, ret } = logger.catch(() => {
return parseCss(text);
});
if (info.some(v => v.level > LogLevel.LOG)) {
cssError.value = '语法错误';
return;
}
const allow = Danmaku.checkCSSAllow(ret);
if (allow.length > 0) {
cssError.value = allow[0];
return;
} else {
cssError.value = '';
}
danmaku.css(ret);
}
function inputColor(color: string) {
nowColor.value = color;
if (fillOpened.value) {
danmaku.textColor = color;
} else {
danmaku.strokeColor = color;
}
}
function addIcon(icon: AllIds | 'hero') {
const iconText = `[i:${icon}]`;
if (iconText.length + danmaku.text.length > 200) {
tip('warn', '弹幕长度超限!');
return;
}
danmaku.text += iconText;
inputValue.value = danmaku.text;
}
function getAllIcons() {
return [
...new Set(
Object.values(core.maps.blocksInfo)
.filter(v => v.cls !== 'tileset')
.map(v => {
return v.id;
})
)
];
}
function clickOuter() {
if (!iconAll.value) close();
else iconAll.value = false;
}
let lockedBefore = false;
onMounted(() => {
mainDiv = document.getElementById('danmaku-editor') as HTMLDivElement;
mainDiv.classList.add('danmaku-startup');
gameKey.disable();
core.lockControl();
mainDiv.addEventListener('focus', () => {
lockedBefore = core.status.lockControl;
core.lockControl();
gameKey.disable();
});
mainDiv.addEventListener('blur', () => {
gameKey.enable();
if (!lockedBefore) core.unlockControl();
});
document.addEventListener('click', clickOuter);
});
onUnmounted(() => {
if (danmaku.text !== '' || Object.keys(danmaku.style).length > 0) {
Danmaku.lastEditoredDanmaku = danmaku;
} else {
delete Danmaku.lastEditoredDanmaku;
}
if (!lockedBefore) core.unlockControl();
gameKey.enable();
document.removeEventListener('click', clickOuter);
});
</script>
<style lang="less" scoped>
#danmaku-editor {
position: fixed;
width: 100%;
bottom: 10px;
display: flex;
align-items: center;
flex-direction: column-reverse;
justify-content: end;
font-size: 200%;
background-color: #000b;
}
.danmaku-startup {
animation: editor-startup 0.4s ease-out 0s 1 normal forwards running;
}
.danmaku-close {
animation: editor-close 0.4s ease-in 0s 1 normal forwards running;
}
@keyframes editor-startup {
0% {
transform: translateY(70px);
}
100% {
transform: none;
}
}
@keyframes editor-close {
0% {
transform: none;
}
100% {
transform: translateY(110px);
}
}
#danmaku-input {
height: 40px;
width: 60%;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 10px;
margin-top: 10px;
}
#danmaku-input-div {
width: 100%;
height: 100%;
display: flex;
align-items: center;
#danmaku-input-input {
width: 100%;
height: 100%;
font-size: 80%;
}
}
.danmaku-tool {
cursor: pointer;
color: white;
transition: color 0.2s linear;
margin-right: 7px;
}
.danmaku-tool[open='true'],
.danmaku-tool:hover {
color: aqua;
}
.danmaku-post {
margin-left: 7px;
}
.danmaku-post[posting='true'] {
color: gray;
cursor: wait;
}
#danmaku-css {
width: 60%;
font-size: 60%;
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
font-family: 'Fira Code';
#danmaku-css-input {
width: 100%;
height: 100%;
font-size: 80%;
margin: 0 10px;
}
#danmaku-css-error {
color: lightcoral;
}
}
#danmaku-icon {
width: 60%;
display: flex;
align-items: center;
font-size: 80%;
white-space: nowrap;
justify-content: space-between;
.danmaku-icon-scroll {
width: calc(90% - 200px);
}
#danmaku-icon-div {
display: flex;
align-items: center;
width: 100%;
}
.danmaku-icon-one {
display: flex;
align-items: center;
}
}
#danmaku-icon-all-div {
position: fixed;
bottom: 110px;
height: 50vh;
background-color: #000b;
border-radius: 20px;
padding: 1%;
font-size: 75%;
width: 60%;
display: flex;
flex-direction: column;
align-items: center;
color: #dddd;
#danmaku-all-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.danmaku-all-scroll {
height: calc(100% - 70px);
}
}
#danmaku-color {
width: 60%;
display: flex;
align-items: center;
white-space: nowrap;
justify-content: space-between;
font-family: 'Fira Code';
font-size: 75%;
#danmaku-color-container {
display: flex;
align-items: center;
margin: 10px;
width: 100%;
}
.danmaku-color-one {
width: 30px;
min-width: 30px;
height: 20px;
border-radius: 6px;
margin-right: 7px;
border: 2px solid transparent;
transition: border 0.1s linear;
}
.danmaku-color-one[selected='true'],
.danmaku-color-one:hover {
border: 2px solid gold;
}
.danmaku-color-scroll {
width: calc(100% - 400px);
}
#danmaku-color-input {
width: 200px;
}
}
.danmaku-enter-active,
.danmaku-leave-active {
transition: all 0.4s ease-out;
position: absolute;
bottom: 50px;
}
.danmaku-enter-from,
.danmaku-leave-to {
opacity: 0;
transform: translateY(50px);
}
.danmaku-icon-enter-active,
.danmaku-icon-leave-active {
transition: all 0.3s ease-out;
}
.danmaku-icon-enter-from,
.danmaku-icon-leave-to {
opacity: 0;
transform: translateY(50px);
}
@media screen and (max-width: 600px) {
#danmaku-input {
width: 90%;
}
#danmaku-css {
width: 90%;
}
#danmaku-icon {
width: 90%;
}
}
</style>