首页前端开发VUEvue3 composition-api实现游动锦鲤 喜欢的可以自己动手实现哦很有成就感的

vue3 composition-api实现游动锦鲤 喜欢的可以自己动手实现哦很有成就感的

时间2023-07-09 09:54:01发布访客分类VUE浏览440
导读:制作一个 myFish 锦鲤组件通过CSS Transform 改变锦鲤坐标, <template> <div> <div class="FishRoot" :style="style" >...

制作一个  myFish 锦鲤组件


通过CSS Transform 改变锦鲤坐标,

template>
    
  div>
    
    div class="FishRoot" :style="style" >
    /div>
    
  /div>
    
/template>
    
script setup lang="ts">

import {
computed}
     from "vue";

const props = defineProps({

  x: Number,
  y: Number,
  angle: Number,
  color: String,
  scale: Number
}
    )
const scale = props.scale || 1;
    
const style = computed(()=>
{

  return `color: ${
props.color}
    ;
transform: translate(${
props.x}
px, ${

      props.y
  }
px) rotate(${
props.angle}
deg) scale(${
scale}
, ${

      scale * 0.8
  }
) rotate(${
-45}
    deg);
    `;

}
    )
/script>
    
style lang="scss" scoped>

.FishRoot {
    
  box-sizing: border-box;
    
  position: absolute;
    
  top: 0;
    
  left: 0;
    
  width: 20px ;
    
  height: 20px ;
    
  background-color: transparent;
    
  border-color: currentColor;
    
  border-style: solid;
    
  border-width: 0 17px  17px  0;
    
  border-radius: 6px ;
    
  opacity: 0.7;
    
  will-change: transform;
    
  pointer-events: none;
    
&
::after {
    
   position: absolute;
    
   content: "";
    
   width: 5px ;
    
   height: 5px ;
    
   right: -22px ;
    
   bottom: -22px ;
    
   background-color: transparent;
    
   border-color: currentColor;
    
   border-style: solid;
    
   border-width: 7px 0 0 7px ;
    
   border-radius: 2px ;
    
   transform-origin: left top;

 }
    
&
::before {
    
   position: absolute;
    
   content: "";
    
   width: 3px ;
    
   height: 3px ;
    
   left: 9px ;
    
   top: 3px ;
    
   background-color: #111;
    
   border-radius: 3px ;

 }

}
    
/style>
    


创建 FishLayer 组件来排列多条锦鲤

template>
    
  !--显示指定数量的锦鲤-->
    
  div class="FishLayerRoot">
    
      my-fish
          v-for="fishProps in stageState.fishList"
          :key="fishProps.id"
          class="FishElement"
          :x="fishProps.position.x"
          :y="fishProps.position.y"
          :angle="fishProps.angle"
          :color="fishProps.color"
          :scale="fishProps.scale"
      />
    
  /div>
    
/template>
    
script lang="ts">

import {
computed, defineComponent, reactive}
     from "vue";

import {
 FishModel }
     from "../core/FishModel";

import {
 Point }
     from "../core/Point";

import {
useMouse}
     from "../core/useMouse";

import {
 useAnimationFrame }
     from "../core/useAnimationFrame";

import {
 useClick }
     from "../core/useClick";
    
import MyFish from "./myFish.vue";

// 锦鲤状态,
type StageState = {
    
  fishList: FishModel[];
 //创建了一个类来定义锦鲤的状态和运动
}
    ;

export default defineComponent({

  components: {
MyFish}
,
  props: {

    //最大锦鲤数
    maxFish: {

      type: Number,
      default: 10
    }
,
  }
,
  setup(props,ctx) {
    
    // 状态
    const stageState = reactiveStageState>
({

      fishList:[]
    }
)
    // 使用鼠标坐标作为状态
    const {
mousePos:destination}
     = useMouse();
     //管理指针坐标
    // 现在的锦鲤数
    const fishCount = computed(()=>
    stageState.fishList.length);
    
    // 更新锦鲤的位置和速度
    const updateFish = () =>
 {
    
      const destPoint = new Point(destination.x, destination.y);
    
      stageState.fishList.forEach((fish) =>
     fish.update(destPoint));

    }
    ;
    
    //添加锦鲤数量
    const addFish = ()  =>
 {
    
      stageState.fishList.push(new FishModel());
    
      ctx.emit("count-changed", fishCount.value);

    }
    ;
    
    // 移除锦鲤
    const removeFish = () =>
 {
    
      stageState.fishList.shift();
    
      ctx.emit("count-changed", fishCount.value);

    }
    
    // 更新锦鲤状态
    useAnimationFrame(()=>
 {
    
      updateFish();

      if (fishCount.value  props.maxFish){
    
        addFish();

      }
    else if (fishCount.value >
 props.maxFish){
    
        removeFish();

      }

      //如果它返回 true,它将被重复调用直到销毁
      return true
    }
    );
    
    //点击动作,施加与光标方向相反的力,使锦鲤逃脱
    useClick(() =>
 {
    
      stageState.fishList.forEach((fish) =>
    
          fish.setForce(-1 - Math.random() * 4)
      );

    }
    );

    return {

      stageState
    }

  }

}
    )
/script>
    

创建了一个 FishModel 类来定义锦鲤的状态和运动

import {
 Point }
     from "./Point";
    
const DEFAULT_FORCE = 0.25;
    
const FORCE_DECREMENT_RATE = 0.03;
    
let instanseCount = 0;
    
const random = (min: number, max: number) =>
     min + (max - min) * Math.random();
    
const randomPoint = (): Point =>
 {
    
    return new Point(
        Math.random() * window.innerWidth,
        Math.random() * window.innerHeight
    );

}
    ;
    
const randomFishColor = (): string =>
 {
    
    const isRed = Math.random()  0.8;

    return isRed
        ? `hsl(${
random(0, 20)}
, 80%, 60%)`
        : `hsl(${
random(240, 260)}
    , 30%, 40%)`;

}
    ;
    
const randomScale = (): number =>
 {
    
    return Math.random() * 0.5 + 0.6;

}
    ;

export class FishModel {
    
    readonly id = instanseCount++;
    
    //位置
    position = randomPoint();
    
    // 方向
    angle = Math.random() * 360;
    
    // 速度矢量图
    vector = new Point();
    
    //目标导向的力量
    force = DEFAULT_FORCE;
    
    //颜色
    color = randomFishColor();
    
    // 缩放
    scale = randomScale();
    
   
    insensitiveTerms = 0;

    //更新锦鲤速度和位置
    update(destPoint: Point) {
    
        const MAX_SPEED = 3;

        this.force = this.force * (1 - FORCE_DECREMENT_RATE) + DEFAULT_FORCE * FORCE_DECREMENT_RATE
        if (this.insensitiveTerms = 0) {
    
            const distVec = destPoint.sub(this.position);
    
            const dist = distVec.length;
    
            const aVec = distVec.unit((dist * this.force) / 100);
    
            this.vector = this.vector.add(aVec).limit(MAX_SPEED);

            if (dist  20) {
    
                this.vector = this.vector.rotate(random(-70, 70));
    
                this.insensitiveTerms = random(20, 30);

            }

        }
 else {
    
            this.insensitiveTerms--;

        }
    
        this.angle = this.vector.angle + 180;
    
        this.position = this.position.add(this.vector);

    }

  //设置锦鲤向“目标点”移动的力。如果你指定一个负值,它会排斥并从该点逃逸
    setForce(value: number) {
    
        this.force = value;

    }
    


创建一个Point类

export class Point {
    
    readonly x: number;
    
    readonly y: number;

    constructor(x = 0, y = 0, a = 0) {
    
        this.x = x;
    
        this.y = y;

    }

    get length(): number {
    
        return Math.sqrt(this.x ** 2 + this.y ** 2);

    }

    get angle(): number {
    
        const rad2angle = (r: number): number =>
     (r / Math.PI) * 180;
    
        return rad2angle(Math.atan2(this.y, this.x));

    }

    add(p: Point): Point {
    
        return new Point(this.x + p.x, this.y + p.y);

    }

    sub(p: Point): Point {
    
        return new Point(this.x - p.x, this.y - p.y);

    }

    times(n: number): Point {
    
        return new Point(this.x * n, this.y * n);

    }

    unit(unitLength = 1): Point {
    
        const len = this.length;
    
        return new Point((this.x / len) * unitLength, (this.y / len) * unitLength);

    }

    limit(maxLength = 1): Point {
    
        return this.length = maxLength ? this : this.unit(maxLength);

    }

    rotate(deg: number): Point {
    
        const angle2rad = (a: number): number =>
     (a * Math.PI) / 180;
    
        const rad = angle2rad(this.angle + deg);
    
        const l = this.length;
    
        return new Point(Math.cos(rad) * l, Math.sin(rad) * l);

    }

}
    

用useMouse单独管理指针坐标

useMouse是在composition-api的讲解中几乎总是会出现的一个sample,但它其实是编写交互式事件处理时使用composition-api的一种非常有效的方式。


import {
 onMounted, reactive, onUnmounted }
     from "vue";
    
export const useMouse = (targetDom?: HTMLElement) =>
 {

    const mousePos = reactive({

        x: 0,
        y: 0,
    }
    );
    
    //可以通过“在PC上移动光标”或“在手机上触摸并滑动”来引导锦鲤
    const onMove = (ev: PointerEvent): void =>
 {
    
        mousePos.x = ev.clientX;
    
        mousePos.y = ev.clientY;

    }
    ;
    
    const onMoveTouch = (ev: TouchEvent): void =>
 {
    
        mousePos.x = ev.touches[0].clientX;
    
        mousePos.y = ev.touches[0].clientY;

    }
    ;
    
    onMounted(() =>
 {
    
        const target = targetDom ?? document.body;
    
        target.addEventListener("pointermove", onMove);
    
        target.addEventListener("touchmove", onMoveTouch);

    }
    );
    
    onUnmounted(() =>
 {
    
        const target = targetDom ?? document.body;
    
        target.removeEventListener("pointermove", onMove);
    
        target.removeEventListener("touchmove", onMoveTouch);

    }
    );

    return {

        mousePos,
    }
    ;

}
    ;
    

requestAnimationFrame 的管理

在没有动画库的情况下实现交互式游戏或动画表达式时,window.requestAnimation会大量使用计时器。这种代码也让组件变长,可读性降低,所以最好用composition-api暴露出来。


import {
 onMounted, onBeforeUnmount }
     from "vue";
    
//返回 true 以继续调用下一帧
export const useAnimationFrame = (onFire: () =>
     boolean) =>
 {
    
    let isTerminated = false;
    
    onMounted(() =>
 {
    
        const tick = () =>
 {
    
            requestAnimationFrame(() =>
 {

                if (isTerminated) {
    
                    return;

                }
    
                const shouldContinue = onFire();

                if (shouldContinue) {
    
                    tick();

                }

            }
    );

        }
    ;
    
        tick();

    }
    );
    
    onBeforeUnmount(() =>
 {
    
        isTerminated = true;

    }
    );

    return {
}
    ;

}
    ;
    

绘制一个背景组件 StageBg

template>
    
  !-- 绘制背景的组件 -->
    
  div class="StageBgRoot">
    
    transition-group name="list">

      div
          class="Stone"
          v-for="stone in stones"
          :key="stone.id"
          :style="{

          left: `calc(${
stone.x * 100}
% - ${
stone.size / 2}
px)`,
          top: `calc(${
stone.y * 100}
% - ${
stone.size / 2}
px)`,
          width: `${
stone.size}
px`,
          height: `${
stone.size}
px`,
          backgroundColor: stone.color,
        }
    "
      />
    
    /transition-group>
    
    slot />
    
  /div>
    
/template>
    
script lang="ts">

import {
 defineComponent, ref }
     from "vue";

import {
 useTicker }
     from "../core/useTicker";

type StoneModel = {
    
  id: number;
    
  x: number;
    
  y: number;
    
  size: number;
    
  color: string;

}
    ;
    
const randomStoneColor = (sizeRate: number): string =>
 {
    
  const isRed = sizeRate  0.25;
    
  const l = (1 - sizeRate) * 50 + 10;

  return isRed
      ? `hsl(${
0 + Math.random() * 30}
, 70%, 70%, ${
1 - sizeRate}
)`
      : `hsl(${
170 + Math.random() * 50}
, 30%, ${
l}
%, ${
1 - sizeRate}
    )`;

}
    ;
    
const createStone = (): StoneModel =>
 {
    
  const sizeR = Math.random();

  return {

    id: Math.random(),
    x: Math.random(),
    y: Math.random(),
    size: 20 + sizeR ** 2 * 200,
    color: randomStoneColor(sizeR),
  }
    ;

}
    ;
    
const MAX_STONE = 50;

export default defineComponent({

  name: "StageBg",
  setup(_, ctx) {
    
    const stones = refStoneModel[]>
    ([]);
    
    const addStone = () =>
 {
    
      stones.value.push(createStone());
    
      if (stones.value.length >
 MAX_STONE) {
    
        stones.value.shift();

      }

    }
    ;
    
    useTicker(addStone, 400);

    return {

      stones,
    }
    ;

  }
,
}
    );
    
/script>
    
style lang="scss" scoped>

.Stone.list-enter-from, .Stone.list-leave-to {
    
  opacity: 0;

}

.Stone {
    
  position: absolute;
    
  border-radius: 100%;
    
  opacity: 1;
    
  transition: opacity 5s;

}

.StageBgRoot {
    
  position: absolute;
    
  left: 0;
    
  top: 0;
    
  width: 100%;
    
  height: 100%;
    
  overflow: hidden;

}
    
/style>

useTicker 规定时间内增加的石头
import {
 onMounted, onUnmounted }
     from "vue";
    
export const useTicker = (onTick: () =>
     void, interval = 1000) =>
 {
    
    let timer = 0;
    
    onMounted(() =>
 {
    
        timer = window.setInterval(onTick, interval);

    }
    );
    
    onUnmounted(() =>
 {
    
        window.clearInterval(timer);

    }
    );

    return {
}
    ;

}
    ;
    

背景和锦鲤组合成一个组件 stageFish

template>
    
  !--合成背景、锦鲤-->
    
  stage-bg class="StageRoot" >
    
    FishLayer :maxFish="maxFish" @count-changed="fishCountChanged" />
    
  /stage-bg>
    
/template>
    
script lang="ts">
    
import FishLayer from "./FishLayer.vue";

import {
defineComponent}
     from "vue";
    
import StageBg from "./stageBg.vue";

export default defineComponent({

  name: "stageFish",
  components: {
StageBg, FishLayer}
,
  props: {

    maxFish: {
 type: Number, default: 50 }
,
  }
,
  setup(props,ctx) {
    
    //锦鲤数量变化时的事件
    const fishCountChanged = (count: number) =>
 {
    
      ctx.emit('count-changed', count);

    }

    return {

      fishCountChanged
    }

  }

}
    )
/script>
    


使用

template>
    
  div id="app">
    
    div class="Control">
    
      button @click="addFish">
    Add 10 Fish/button>
    
      button @click="removeFish">
    Remove 10 Fish/button>
    
      span>
fish count = {
{
 fishCount }
}
    /span>
    
    /div>
    
    stage-fish :maxFish="maxFish" @count-changed="fishCountChanged" />
    
  /div>
    
/template>
    
script setup lang="ts">

import {
ref}
     from "vue";
    
import StageFish from "./components/stageFish.vue";
    
const maxFish = ref(10)
const fishCount = ref(0)
const addFish = () =>
 {
    
  maxFish.value += 2;

}
    
const removeFish = () =>
 {
    
  maxFish.value = Math.max(0, maxFish.value - 10);

}
    
const fishCountChanged = (count: number) =>
 {
    
  fishCount.value = count;

}
    
/script>
    
style lang="scss">

* {
    
  box-sizing: border-box;

}

html,
body {
    
  margin: 0;
    
  padding: 0;
    
  position: relative;
    
  height: 100%;
    
  background-color: rgb(31, 36, 43);
    
  color: rgb(80, 110, 124);
    
  font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
    
  overflow: hidden;

}

button {
    
  display: inline-block;
    
  margin-right: 5px;
    
  border: 2px solid rgb(80, 110, 124);
    
  color: rgb(80, 110, 124);
    
  font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
    
  padding: 2px 5px;
    
  background-color: transparent;

}

#app {
    
  position: absolute;
    
  width: 100%;
    
  height: 100%;

}

.Control {
    
  position: absolute;
    
  z-index: 1;
    
  width: 100%;
    
  padding: 5px;
    
  background-color: #00000066;

}
    
/style>
    

声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!


若转载请注明出处: vue3 composition-api实现游动锦鲤 喜欢的可以自己动手实现哦很有成就感的
本文地址: https://pptw.com/jishu/298117.html
HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第5章CSS盒子模型(下) vue3鼠标经过显示按钮

游客 回复需填写必要信息