“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

  1. dialogBox: Bottom dialog box component
  2. lottieInput:The spellAfter an Easter egg explosion effect component
  3. spriteSprite map animation component
  4. typedThe inputThe spellTyping 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! .