“I am participating in the Mid-Autumn Festival Creative Submission Contest. Please see: Mid-Autumn Festival Creative Submission Contest for details.”
preface
Again the weekend time, idle at home, spent two days to design and produce a Mid-Autumn festival related page, first grounding technology stack gas and with the current new technology, so I consider using Vue3 + Typescript, the second is the Mid-Autumn festival theme, I think of the story of chang e, since it is chang e, then the page is interesting Taste and playfulness. So I ended up doing something like this.
The tech stack is chosen and the crafting theme and style is chosen. The liver has been dried for a day, and the following is the result after production
First, let’s talk about the script, which is about the characters of Hou Yi (Two cows) and Chang ‘e, plus the Dali section of the king’s rising bridge. And finally a ghost livestock flying effect, I say first, this is really didnt find available material, can only make do with the Internet to find this animation. O (╥﹏╥) O good, then begin to say, I am how to achieve this kind of game page animation effect.
Page organization
The pages are created using Vite, and the file structure looks like this
Since the page has only one scene, the entire page is written in app.vue. The interface folder holds some interface objects defined. The components are divided into four components, in order
dialogBox
: Bottom dialog box componentlottie
Input:The spell
After an Easter egg explosion effect componentsprite
Sprite map animation componenttyped
The inputThe spell
Typing effects component
So let’s follow the animation effect of the page to talk about it.
Sprite animation
At the beginning of the page, there is an animation of two Cows walking up the bridge from the left. Let’s analyze this animation first. The first is the frame animation, which is the effect of the action of walking, and the second is the displacement animation of walking up the bridge from the left. So let’s talk about frame animation first
The frame of animation
“Frame-by-frame animation is a common form of animation (Frame By Frame). Its principle is to break down the animation action in” continuous keyframes “, that is, to draw different content on each Frame of the timeline Frame By Frame, so that it can be continuously played into animation
Taking my project as an example, the animation of Two Cows walking is actually a picture in front of us, which is also called Sprite picture. There are four movements in the picture, and when the four movements are constantly switching, the dynamic effect of walking is formed in our eyes. Ok, that explains the principle, so now let’s look at the code
<div ref="spriteBox">
<div ref="sprite" class="sprite"></div>
</div>
Copy the code
The structure of the page is very simple, just three lines of HTML code, the HTML wrapped outside is actually used to do displacement animation, Sprite inside is to do frame animation. Let’s take a look at the javascript code
// Style position
exportinterface positionInterface { left? : string, top? : string, bottom? : string, right? : string }export interface spriteInterface {
length: number, // The length of the Sprite diagram
url: string, // The path to the image
width: number, // Width of the image
height: number, // Height of the imagescale? : number,/ / zoom
endPosition: positionInterface // The position of the animation end station
}
import { Ref } from "vue";
import { positionInterface, spriteInterface } from ".. /.. /interface";
/** * Sprite map to achieve frame-by-frame animation *@param SpriteObj Sprite object@param Target Sprite node *@param Wrap Sprite parent node [control Sprite movement] *@param Callback picture loads the callback function *@param MoveCallback moves to the corresponding position of the callback */
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();
// Stop if the specified position is reached
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++;
// Use setTimeout, requestFrameAnimation is 60HZ for rendering, some devices will freeze, use setTimeout to manually control the rendering time
time = setTimeout(autoLoop, 160); }) (); });// Go to the corresponding position
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; }}Copy the code
parameter
UseFrameAnimation useFrameAnimation is a frame animation function that first passes the Sprite graph description object, which describes the Sprite graph is composed of several actions, the address of the image, the object of the image on the DOM node, and the callback function passed to the parent of the calling function after moving to the specified position. In fact, the comments in the code are very clear.
Image to load
When we use this image for animation, we first have to deal with the image after it is loaded. So we need to create a new Image, assign SRC to it, and then listen for its load event,
Looping animation
In the load event handle, a loop is written to toggle the backgroundPositionX property of the image to achieve the switch of the page action picture. Since it is a loop animation, if the animation reaches the last image, it has to cut back to the first image
Add a callback function hook
When the image load is complete, a callback function is called to tell the outside world that the image load is complete. If something needs to be done to complete the image load, it can be written in this callback function. There is also an isEnd function in the code to determine whether the displacement animation is complete, and if so, to stop the frame animation loop and let it stand still as a picture. MoveCallback then tells the parent of the calling function that the displacement animation has been executed. That’s basically what this function does.
Displacement of the animation
The displacement animation is relatively simple. Let’s look at the code first:
<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"); }); }});// Add units to the width
const widthRef = computed(() = > {
return width.value + "px";
});
// Add units to the height
const heightRef = computed(() = > {
return height.value + "px";
});
// Add the url to the background image link
const urlImg = computed(() = > {
return `url("${url.value}") `;
});
// Move to target position
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>Copy the code
The main thing in the code is this watchEffect, Use the useFrameAnimation function (usFrameAnimation) to call the useFrameAnimation function (triggerMov). When the image is loaded, we can animate it in this position E, the triggerMove function actually puts the position and scaling information configured in spriteObj onto the corresponding DOM node, and when it comes to animation, CSS does that. Pass a moveEnd custom event to the parent after the listener is in place to move the drawing.
<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>
Copy the code
The CSS here only describes the width and height of the Sprite map and the image path. The above method of v-bind is used after Vue3, so that you can write dynamic variables directly in the 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);
}
Copy the code
The above describes the initial positions of erniu and Chang ‘e, as well as the dynamic effects.
Dialog box component
After Erniu walks to Chang ‘e,APP. Vue knows the end of the animation through the moveEnd custom event mentioned above, and then pops up a dialog box after the animation ends. Dialogue, in fact, we must first think of a dialogue script and dialogue script format.
Dialogue script
const dialogueContent = [
{
avatar: "/images/rpg_male.png".content: "Two cows: Chang 'e you finally willing to date with me, ha ha"}, {avatar: "/images/rpg_female.png".content: "Chang e: two cows sorry, I come from the moon palace, I can't and the world of you together!"}, {avatar: "/images/rpg_female.png".content:
"Chang 'e: Today is the Mid-Autumn Festival. Today is the only chance I have to go back to the moon."}, {avatar: "/images/rpg_female.png".content:
"Chang e: back to the moon palace condition is to find a true person, let him read the spell, I can fly!"}, {avatar: "/images/rpg_female.png".content: "Chang e: and you are my true person, can you help me?"}, {avatar: "/images/rpg_male.png".content: Bull: OK, I see! I'll help you."}, {avatar: "/images/rpg_female.png".content: Chang 'e: Ok. Thank you!",},];Copy the code
Above is my script of this small game, because others say a paragraph first, I say a paragraph again, or others say a paragraph, and then say a paragraph. In this case, just write it down in the order of the conversation, and then we can display it one by one in the code by clicking the interaction of time. The main structure of the dialogue is the avatar and the content of the character. To save trouble, I have directly displayed the name of the character in the content. In fact, if necessary, you can mention it.
structure
Let’s look at its HTML structure first
<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>
Copy the code
In fact, the structure is very simple, there is only an avatar and content, we use isShow to control the display and hiding of the dialog box, and use increase to go to the next dialog.
Logic implementation
function increase() {
dialogueIndex.value++;
if (dialogueIndex.value >= dialogueArr.length) {
isShow.value = false;
emit("close");
return;
}
// Make the next content look like typing
contentRef.value = useType(dialogue.value.content);
}
Copy the code
The increase method is also very simple. After clicking on it, the declared index (default is 0)+1. If the index is equal to the script length, close the dialog box and give app.vue a close custom event. If it is shorter than the length of the script, go to the next script and type it. This is the useType method.
/** * Typing effect *@param { Object } Content The typed 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
}
Copy the code
Typing effect implementation is also very simple, default to a _, and then get each character of the string, one by one, after the new string. If the complete string is retrieved, the loop is stopped.
Type box (spell) component
After finishing the script, app. vue gets the close custom event that the component runs out of, where we can display the curse component,
structure
<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>
Copy the code
The curse component, which uses the property contenteditable in its HTML structure, allows the div to look like the input box and can be modified directly on the text above it. Incantation is the incantation at the bottom and font is the incantation we need to input.
Logic implementation
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>Copy the code
At the time of components play a window, we use incantainerRef. Value. The focus (); Let it automatically get focus. In the inputChange event, we try to determine whether the inputChange is the same as the inputChange. If the inputChange event is different, the inputChange event cannot continue to input and stays on the correct inputChange event. If the inputChange event is correct, the inputChange event will automatically close the inputChange event and pop up a fireworks effect similar to congratulations. Pass a completeOver custom event to app.vue.
Page theme app.vue
Page, in fact, like a director. After receiving feedback from the actors, the next actor is positioned
setup() {
let isShow = ref(false); // Switch the dialog box window
let typedShow = ref(false); // Spell window switch
let girlAction = ref(false); // The girl moves the switch, the director calls out an action, the actor begins to act
const boy = reactive(boyData);
const girl = reactive(girlData);
const dialogueArr = reactive(dialogueContent);
// The boy moves to the end of animation
function boyMoveEnd() {
isShow.value = true;
}
// Complete the input spell
function completeOver() {
girlAction.value = true;
}
function girlMoveEnd() {}
// The dialog window closes
function dialogClose() {
// After the dialog box closes, the window of the spell pops up. After erniu input the spell, Chang 'e begins to fly the fairy
typedShow.value = true;
}
return {
dialogueArr,
boy,
girl,
isShow,
boyMoveEnd,
girlMoveEnd,
girlAction,
dialogClose,
typedShow,
completeOver,
};
Copy the code
Just take a look. There’s nothing special to say.
Write in the last
I won’t talk about the fireworks effect, because I covered every detail of how to use Lottie in VUE in my last article. And this time the component is actually the reusable component that I’m talking about in this article. This is the basic effect, if you are interested, you can refer to my base and add some details in it. For example, add cloud movement effect, add water wave effect, etc. Need source code can click here to see a full day is so fast ah ~ we see you next time. I wish everyone a happy Mid-Autumn Festival in advance! .