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

简介: 制作一个 myFish 锦鲤组件

制作一个  myFish 锦鲤组件

47115d5882ff46359f59b6b9cc611eb2.gif


通过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 = reactive<StageState>({
      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 = ref<StoneModel[]>([]);
    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>
相关文章
|
3月前
|
JavaScript 前端开发 IDE
[译] 用 Typescript + Composition API 重构 Vue 3 组件
[译] 用 Typescript + Composition API 重构 Vue 3 组件
[译] 用 Typescript + Composition API 重构 Vue 3 组件
|
3月前
|
JavaScript 前端开发 API
[译] 用 Vue 3 Composition API 实现 React Context/Provider 模式
[译] 用 Vue 3 Composition API 实现 React Context/Provider 模式
|
21天前
|
JavaScript 前端开发 API
Vue 3新特性详解:Composition API的威力
【10月更文挑战第25天】Vue 3 引入的 Composition API 是一组用于组织和复用组件逻辑的新 API。相比 Options API,它提供了更灵活的结构,便于逻辑复用和代码组织,特别适合复杂组件。本文将探讨 Composition API 的优势,并通过示例代码展示其基本用法,帮助开发者更好地理解和应用这一强大工具。
25 1
|
1月前
|
缓存 JavaScript API
Vue 3的全新Reactivity API:解锁响应式编程的力量
Vue 3引入了基于Proxy的全新响应式系统,提升了性能并带来了更强大的API。本文通过示例详细介绍了`reactive`、`ref`、`computed`、`watch`等核心API的使用方法,帮助开发者深入理解Vue 3的响应式编程。无论你是初学者还是资深开发者,都能从中受益,构建更高效的应用程序。
19 1
|
2月前
|
JavaScript 前端开发 API
花了一天的时间,地板式扫盲了vue3中所有API盲点
这篇文章全面介绍了Vue3中的API,包括组合式API、选项式API等内容,旨在帮助开发者深入了解并掌握Vue3的各项功能。
花了一天的时间,地板式扫盲了vue3中所有API盲点
|
1月前
|
缓存 JavaScript API
Vue 3的全新Reactivity API:解锁响应式编程的力量
【10月更文挑战第9天】Vue 3的全新Reactivity API:解锁响应式编程的力量
19 3
|
1月前
|
缓存 JavaScript 前端开发
深入理解 Vue 3 的 Composition API 与新特性
本文详细探讨了 Vue 3 中的 Composition API,包括 setup 函数的使用、响应式数据管理(ref、reactive、toRefs 和 toRef)、侦听器(watch 和 watchEffect)以及计算属性(computed)。我们还介绍了自定义 Hooks 的创建与使用,分析了 Vue 2 与 Vue 3 在响应式系统上的重要区别,并概述了组件生命周期钩子、Fragments、Teleport 和 Suspense 等新特性。通过这些内容,读者将能更深入地理解 Vue 3 的设计理念及其在构建现代前端应用中的优势。
32 0
深入理解 Vue 3 的 Composition API 与新特性
|
2月前
|
存储 JavaScript 前端开发
敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs
该文章深入探讨了Vue3中Composition API的关键特性,包括`ref`、`toRef`、`toRefs`的使用方法与场景,以及它们如何帮助开发者更好地管理组件状态和促进逻辑复用。
敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs
|
1月前
|
JavaScript API
|
26天前
|
API
《vue3第四章》Composition API 的优势,包含Options API 存在的问题、Composition API 的优势
《vue3第四章》Composition API 的优势,包含Options API 存在的问题、Composition API 的优势
26 0