vue3 composition-api实现游动锦鲤 喜欢的可以自己动手实现哦很有成就感的
导读:制作一个 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
