制作一个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 = 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>
评论(0)