«Я участвую в творческом конкурсе Праздника середины осени, смотрите:Творческий конкурс «Праздник середины осени»"
предисловие
Снова выходные, мне нечего делать дома, я потратил два дня на то, чтобы придумать и сделать страницу, связанную с Праздником середины осени, прежде всего, стек технологий обоснован и соответствует современным новым технологиям, поэтому я рассматриваю возможность использованияVue3
+Typescript
, а затем тема Праздника середины осени, я думаю об истории о том, как Чанъэ летит на Луну. Поскольку это Чанъэ летит на Луну, страница должна быть интересной и игривой. Поэтому я, наконец, решил сделать страница с похожим стилем.
После выбора стека технологий и темы и стиля производства
Сначала поговорим о сценарии.春光灿烂猪八戒
из后羿(二牛)
а также嫦娥
персона плюс东成西就
из大理段王爷
Секция моста вознесения.Также есть последний эффект вознесения призраков и животных.Сначала скажу,что это потому,что я действительно не нашел доступного материала,поэтому могу обойтись только этой анимацией,найденной в интернете. o(╥﹏╥)o Хорошо, тогда давайте начнем разговор, как я добился эффекта анимации страницы в этом типе игры.
организация страницы
Страница создана с помощью vite, структура файла такая
Поскольку на странице есть только одна сцена, вся страница помещается вAPP.vue
написано в.interface
В папке хранятся некоторые определенные объекты интерфейса.В компоненте 4 компонента, за которыми следуют
-
dialogBox
: Компонент нижнего диалога -
lottie
: Вход咒语
После компонента эффекта взрыва яйца -
sprite
компонент анимации спрайтов -
typed
войти咒语
Компонент «Эффекты ввода»
Итак, давайте поговорим об этом по очереди в соответствии с анимационными эффектами, которые появляются на странице.
спрайт анимация
Страница начинается с二牛
Анимация персонажа, идущего по мосту слева. Давайте сначала разберем эту анимацию.帧动画
, то есть эффект действия ходьбы, за которым следует анимация смещения ходьбы по мосту слева.Тогда поговорим сначала об этом帧动画
Кадровая анимация
«Покадровая анимация — это распространенная форма анимации (Frame By Frame). Ее принцип заключается в разложении анимационных действий на «непрерывные ключевые кадры», то есть отрисовке разного контента кадр за кадром на каждом кадре временной шкалы, чтобы Анимация непрерывного воспроизведения
Возьмите мой проект в качестве примера,二牛
На самом деле анимация ходьбы — это картинка, в нашем интерфейсе эта картинка также называется雪碧图
,На картинке 4 действия.При постоянном переключении 4 действий у нас на глазах формируется эффект шагающего движения.Хорошо принцип объяснен понятно,тогда давайте теперь посмотрим на код
<div ref="spriteBox">
<div ref="sprite" class="sprite"></div>
</div>
Структура страницы очень простая, всего три строчки.html
код, завернутыйhtml
На самом деле, раньше это делалось位移动画
б/у, внутриsprite
просто делать帧动画
, Давайте посмотрим на код javascript
// 样式位置
export interface positionInterface {
left?: string,
top?: string,
bottom?: string,
right?: string
}
export interface spriteInterface {
length: number, // 精灵图的长度
url: string, // 图片的路径
width: number, // 图片的宽度
height: number, // 图片的高度
scale?: number, // 缩放
endPosition: positionInterface // 动画结束站的位置
}
import { Ref } from "vue";
import { positionInterface, spriteInterface } from "../../interface";
/**
* 精灵图实现逐帧动画
* @param spriteObj 精灵对象
* @param target 精灵节点
* @param wrap 精灵父节点 [控制精灵移动]
* @param callback 图片加载好回调函数
* @param moveCallback 移动到对应位置的回调函数
*/
export function useFrameAnimation(
spriteObj: spriteInterface,
target: Ref,
wrap: Ref,
callback: Function,
moveCallback: Function
) {
const { width, length, url, endPosition } = spriteObj;
let index = 0;
var img = new Image();
img.src = url;
img.addEventListener("load", () => {
let time;
(function autoLoop() {
callback && callback();
// 如果到达了指定的位置的话,则停止
if (isEnd(wrap, endPosition)) {
if (time) {
clearTimeout(time);
time = null;
moveCallback && moveCallback();
return;
}
}
if (index >= length) {
index = 0;
}
target.value.style.backgroundPositionX = -(width * index) + "px";
index++;
// 使用setTimeout, requestFrameAnimation 是60HZ进行渲染,部分设备会卡,使用setTimeout可以手动控制渲染时间
time = setTimeout(autoLoop, 160);
})();
});
// 走到了对应的位置
function isEnd(wrap, endPosition: positionInterface) {
let keys = Object.keys(endPosition);
for (let key of keys) {
if (window.getComputedStyle(wrap.value)[key] === endPosition[key]) {
return true;
}
}
return false;
}
}
параметр
useFrameAnimation
это帧动画
функция, параметры функции передаются первыми精灵图
Объект описания, который в основном описывает изображение спрайта, состоит из нескольких действий, что такое адрес изображения, объект изображения на узле DOM и функция обратного вызова, передаваемая родителю вызывающей функции после перехода к На самом деле, комментарии в коде также описывают это очень четко.
загрузка изображения
Мы используем это изображение, чтобы сделать帧动画
Когда изображение загружено, оно должно быть сначала обработано после загрузки изображения, поэтому мы должны сначалаnew Image
, а затем присвойте емуsrc
, затем следите за егоload
мероприятие,
Перебирать анимации
В обработчике события загрузки напишитеloop
перебирать картинкиbackgroundPositionX
Атрибут достигает переключения изображения действия страницы.Поскольку это циклическая анимация, если анимация достигает последнего изображения, она должна переключиться обратно на первое изображение.
Добавить обработчик функции обратного вызова
Когда изображение загружено, вызовитеcallback
Функция, которая сообщает внешнему изображению, что загрузка завершена.Если есть что-то, что нужно сделать для загрузки изображения, вы можете написать это в этой функции обратного вызова.Есть также код вisEnd
функция судить位移
Была ли анимация завершена, если анимация смещения завершена, остановить帧动画
петля, дайте ей постоять и стать картинкой.Затем выполнитеmoveCallback
Сообщите родителю вызывающей функции, что анимация смещения закончилась.Это примерно то, что делает эта функция.
Анимация смещения
Анимация смещения относительно проста, давайте сначала посмотрим на код:
<script lang="ts">
import {
computed,
defineComponent,
defineEmit,
PropType,
reactive,
ref,
toRefs,
watchEffect,
} from "vue";
import { spriteInterface } from "../../interface";
import { useFrameAnimation } from "./useFrameAnimation";
export default defineComponent({
props: {
action: {
type: Boolean,
default: false,
},
spriteObj: Object as PropType<spriteInterface>,
},
defineEmit: ["moveEnd"],
setup(props, { emit }) {
const spriteBox = ref(null);
const sprite = ref({ style: "" });
const spriteObj = reactive(props.spriteObj || {}) as spriteInterface;
const { width, height, url, length } = toRefs(spriteObj);
watchEffect(() => {
if (props.action) {
useFrameAnimation(
spriteObj,
sprite,
spriteBox,
() => {
triggerMove();
},
() => {
emit("moveEnd");
}
);
}
});
// 给宽度后边加上单位
const widthRef = computed(() => {
return width.value + "px";
});
// 给高度后边加上单位
const heightRef = computed(() => {
return height.value + "px";
});
// 给背景图片连接添加url
const urlImg = computed(() => {
return `url("${url.value}")`;
});
// 移动到目标位置
function triggerMove() {
if (spriteObj.scale || spriteObj.scale === 0) {
spriteBox.value.style.transform = `scale(${spriteObj.scale})`;
}
if (spriteObj.endPosition) {
Object.keys(spriteObj.endPosition).forEach((o) => {
if (spriteBox.value && sprite.value.style) {
spriteBox.value.style[o] = spriteObj.endPosition[o];
}
});
}
}
return {
widthRef,
heightRef,
urlImg,
length,
sprite,
spriteBox,
triggerMove,
};
},
});
</script>
Главное в коде этоwatchEffect
, судя по использованию精灵组件
Передайте props.action, чтобы начать решать, начинать ли帧动画
, после вызова того, о чем мы говорили в предыдущем пунктеuseFrameAnimation
После функции четвертый параметр функции обратного вызова заключается в том, что изображение загружается Когда изображение загружается, мы можем сделать это здесь.位移动画
, то есть,triggerMove
,triggerMove
Функция на самом деле вставляетspriteObj
Некоторые настроенные позиции и информация о масштабе помещаются в соответствующиеDOM
На узле, если вы хотите говорить об анимации, это на самом делеcss
Делать в прослушивании位移动画
После окончания передать родителю amoveEnd
пользовательское событие.
<style lang="scss" scoped>
.sprite {
width: v-bind(widthRef);
height: v-bind(heightRef);
background-image: v-bind(urlImg);
background-repeat: no-repeat;
background-position: 0;
background-size: cover;
}
</style>
CSS здесь описывает только о精灵图
ширина, высота и путь к изображению, выше написанное таким образомv-bind
Это способ, который можно использовать после vue3, так что динамические переменные могут быть записаны непосредственно в CSS, и все, кто его использовал, говорят, что это хорошо~精灵图
Настоящий анимационный эффект записывается вAPP.vue
Внутри CSS
.boy {
position: absolute;
bottom: 90px;
left: 10px;
transform: translate3d(0, 0, 0, 0);
transition: all 4s cubic-bezier(0.4, 1.07, 0.73, 0.72);
}
.girl {
position: absolute;
bottom: 155px;
right: 300px;
transform: translate3d(0, 0, 0, 0);
transition: all 4s cubic-bezier(0.4, 1.07, 0.73, 0.72);
}
описано выше二牛
а также嫦娥
Исходное положение и динамические эффекты.
диалоговый компонент
существует二牛
идти嫦娥
рядом после,APP.vue
через вышеупомянутыйmoveEnd
Пользовательское событие знает, что анимация закончилась, а затем после окончания анимации появляется диалоговое окно.Для диалога вам действительно нужно подумать о сценарии диалога и формате сценария диалога.
сценарий диалога
const dialogueContent = [
{
avatar: "/images/rpg_male.png",
content: "二牛:嫦娥你终于肯和我约会了, 哈哈",
},
{
avatar: "/images/rpg_female.png",
content: "嫦娥:二牛对不起,我是从月宫来的,我不能和人间的你在一起!",
},
{
avatar: "/images/rpg_female.png",
content:
"嫦娥:今天是中秋节,我只有今天这个机会可以重新回月宫",
},
{
avatar: "/images/rpg_female.png",
content:
"嫦娥:回月宫的条件是找到真心人,让他念起咒语,我才能飞升!",
},
{
avatar: "/images/rpg_female.png",
content: "嫦娥:而你就是我的真心人,你可以帮我嘛?",
},
{
avatar: "/images/rpg_male.png",
content: "二牛:好的,我明白了! 我会帮你的.",
},
{
avatar: "/images/rpg_female.png",
content: "嫦娥:好的。 谢谢你!",
},
];
Это сценарий моей маленькой игры, потому что кто-то сказал сначала абзац, я сказал другой абзац, или кто-то другой сказал абзац, а затем сказал абзац В этом случае просто запишите это в порядке диалога, а затем мы в Код может отображаться один за другим по порядку, нажав время взаимодействия.Структура диалога в основном人物头像
а также人物内容
,Здесь я показываю имена персонажей прямо в содержании,чтобы не заморачиваться.На самом деле при необходимости их можно подтянуть.
структура
Давайте сначала посмотрим на это.html
структура
<div v-if="isShow" class="rpg-dialog" @click="increase">
<img :src="dialogue.avatar" class="rpg-dialog__role" />
<div class="rpg-dialog__body">
{{ contentRef.value }}
</div>
</div>
Структура на самом деле очень простая, внутри есть аватарка и контент, используемisShow
Для управления отображением и скрытием диалога используйтеincrease
Перейти к следующему разговору.
логическая реализация
function increase() {
dialogueIndex.value++;
if (dialogueIndex.value >= dialogueArr.length) {
isShow.value = false;
emit("close");
return;
}
// 把下个内容做成打字的效果
contentRef.value = useType(dialogue.value.content);
}
существуетincrease
Способ тоже очень простой, после клика заявленный индекс (по умолчанию 0)+1
, если индекс равен длине скрипта, закройте диалоговое окно, а затем дайтеAPP.vue
Одинclose
Пользовательское событие, если оно меньше длины скрипта, перейти к следующему содержимому скрипта и начать с打字
Эффект представлен.useType
метод.
/**
* 打字效果
* @param { Object } content 打字的内容
*/
export default function useTyped(content: string): Ref<string> {
let time: any = null
let i:number = 0
let typed = ref('_')
function autoType() {
if (typed.value.length < content.length) {
time = setTimeout(() =>{
typed.value = content.slice(0, i+1) + '_'
i++
autoType()
}, 200)
} else {
clearTimeout(time)
typed.value = content
}
}
autoType()
return typed
}
Реализация эффекта печати также очень проста, по умолчанию задано_
, а затем получить каждый символ строки один за другим и добавить их в конец новой строки один за другим. Если вы получите полную строку, остановите цикл.
Компонент Typebox (заклинание)
Закончив сценарий,APP.vue
получит компоненты и закончатсяclose
пользовательское событие, в которое мы можем поместить诅咒组件
показывать,
структура
<div v-if="isShow" class="typed-modal">
<div class="typed-box">
<div class="typed-oldFont">{{ incantation }}</div>
<div
@input="inputChange"
ref="incantainerRef"
contenteditable
class="typed-font"
>
{{ font }}
</div>
</div>
</div>
Проклятый компонент, здесьhtml
Структура, мы можем взглянуть, она используется внутриcontenteditable
Это свойство, после установки этого свойства,div
Его можно изменить, чтобы он был похож на поле ввода, мы можем напрямуюdiv
Приведенный выше текст можно свободно изменять, поэтому нам необходимо отслеживать изменения пользователя при его изменении.input
мероприятие.incantation
Это наконечник внизу咒语
, font
Ввод - это то, что нам нужно ввести咒语
.
логическая реализация
export default defineComponent({
components: {
ClickIcon,
},
emits: ["completeOver"],
setup(props, { emit }) {
const isShow = ref(true);
const lottie = ref(null);
const incantainerRef = ref(null);
const defaultOption = reactive(defaultOptions);
const incantation = ref("Happy Mid-autumn Day");
let font = ref("_");
nextTick(() => {
incantainerRef.value.focus();
});
function inputChange(e) {
let text = e.target.innerText.replace("_", "");
if (!incantation.value.startsWith(text)) {
e.target.innerText = font.value;
} else {
if (incantation.value.length === text.length) {
emit("completeOver");
font.value = text;
isShow.value = false;
lottie.value.toggle();
} else {
font.value = text + "_";
}
}
}
return {
font,
inputChange,
incantation,
incantainerRef,
defaultOption,
lottie,
isShow,
};
},
});
</script>
Когда компонент всплывает, мы используемincantainerRef.value.focus();
Сделайте его автоматическим фокусом.inputChange
В случае, мы идем судить ввод咒语
ли и подскажите咒语
то же самое, если отличается, вы не можете продолжать печатать и оставаться на правильном вводе咒语
Вкл., если все введено правильно, он автоматически закроется咒语
всплывающее окно, и всплывающее подобное恭喜通过
Эффект фейерверка.completeOver
специальное событие дляAPP.vue
.
Тема страницы APP.vue
Страница фактически как режиссер.После получения различных отзывов от актеров будет организован следующий актер
setup() {
let isShow = ref(false); // 对话框窗口开关
let typedShow = ref(false); // 咒语窗口开关
let girlAction = ref(false); // 女孩动作开关, 导演喊一句action后,演员开始演绎
const boy = reactive(boyData);
const girl = reactive(girlData);
const dialogueArr = reactive(dialogueContent);
// 男孩移动动画结束
function boyMoveEnd() {
isShow.value = true;
}
// 完成输入咒语
function completeOver() {
girlAction.value = true;
}
function girlMoveEnd() {}
// 对话窗口关闭
function dialogClose() {
// 对话框关闭后,弹出咒语的窗口,二牛输入咒语后,嫦娥开始飞仙动作
typedShow.value = true;
}
return {
dialogueArr,
boy,
girl,
isShow,
boyMoveEnd,
girlMoveEnd,
girlAction,
dialogClose,
typedShow,
completeOver,
};
Просто взгляните на него, сказать особо нечего.
напиши в конце
Я не буду говорить об этом эффекте фейерверка, потому что моя последняя статьяКак использовать Лотти в VueКаждая деталь была подробно объяснена. И на этот раз этот компонент на самом деле является самоинкапсулированным компонентом, о котором я говорил в этой статье. Основные эффекты таковы. Если вам интересно, вы можете обратиться к моей статье. На основе , добавьте в него некоторые детали. Например, добавьте云彩
анимация, добавить水波
Динамические эффекты и т.д. Если вам нужен исходный код, вы можетекликните сюдаСмотри, насыщенный день проходит так быстро~ Увидимся в следующий раз, всем желаю заранее中秋节快乐!
.