console
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>p5js-ball-demo</title>
<meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" name="viewport">
<meta name="renderer" content="webkit">
<style>
* {
margin: 0;
padding: 0;
list-style: none;
}
body {
font-size: 14px;
font-family: sans-serif,"HelveticaNeue",Helvetica,"PingFangSC","MicrosoftYaHei","HiraginoSansGB",Arial;
color: #27282b;
background: #34363f;
}
a {
text-decoration: none;
color: #27282b;
}
*,
*::before,
*::after {
outline: none;
box-sizing: border-box;
}
html,
body {
height: 100vh;
overflow: hidden;
}
.setting-panel {
width: 200px;
height: 270px;
overflow-y: auto;
padding: 20px 20px;
background: #23252af7;
user-select: none;
border-radius: 3px;
position: fixed;
top: -1000px;
left: -1000px;
}
.setting-panel::-webkit-scrollbar {
width: 3px;
}
.setting-panel:hover::-webkit-scrollbar-thumb {
background-color: #191a1c;
}
.d-item:not(:last-child) {
margin-bottom: 12px;
}
.d-item .s-tit {
display: inline-block;
font-size: 12px;
color: #d5d5d5;
margin-bottom: 8px;
}
.d-item-color {
margin-bottom: 0;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
.inp-range {
display: block;
width: 100%;
height: 2px;
border-radius: 50px;
background: #7774;
-webkit-appearance: none;
}
.inp-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px;
height: 10px;
border-radius: 55%;
background: #b5b6b7;
}
.inp-range:hover::-webkit-slider-thumb {
background: #e0e0e0;
}
.inp-color {
border: 0;
width: 18px;
height: 18px;
border-radius: 55%;
margin-left: 5px;
background: #e7e7e7;
}
.inp-text {
width: 50px;
height: 15px;
padding: 0 3px;
color: #d5d5d5;
background: transparent;
border-radius: 2px;
border: 0;
}
.lb-ckb {
width: 100%;
}
.inp-checkbox {
width: 12px;
height: 12px;
margin-right: 2px;
vertical-align: -1px;
}
</style>
</head>
<body>
<div class="wrapper" id="app">
<div class="setting-panel" :style="stStyle" v-show="showSettingPanel">
<div class="d-item">
<p class="s-tit">小球个数: <input class="inp-text" type="number" v-model="dp.pCount"></p>
<input class="inp-range" type="range" min="0" max="200" step="5" v-model.number="dp.pCount">
</div>
<div class="d-item">
<p class="s-tit">小球大小: <input class="inp-text" type="number" v-model="dp.pSize"></p>
<input class="inp-range" type="range" min="0" max="50" step="2" v-model.number="dp.pSize">
</div>
<div class="d-item d-item-color">
<p class="s-tit">小球颜色: </p>
<input class="inp-color" type="color" v-model="dp.pColor">
</div>
<div class="d-item d-item-color">
<label class="s-tit lb-ckb" for="inprndxs">
<input class="inp-text inp-checkbox" id="inprndxs" type="checkbox" v-model="dp.randColor">
随机颜色
</label>
</div>
<div class="d-item">
<p class="s-tit">小球初始速度: <input class="inp-text" type="text" v-model.number="dp.pSpeed"></p>
<input class="inp-range" type="range" min="0" max="30" step="1" v-model.number="dp.pSpeed">
</div>
<div class="d-item">
<p class="s-tit">小球初始位置: {{ getPosText }}</p>
<input class="inp-range" type="range" min="0" max="4" step="1" v-model.number="dp.pPos">
</div>
<div class="d-item">
<p class="s-tit">摩擦系数: <input class="inp-text" type="number" step="0.05" v-model="dp.friction"></p>
<input class="inp-range" type="range" min="0" max="1" step="0.05" v-model.number="dp.friction">
</div>
<div class="d-item">
<p class="s-tit">重力加速度: <input class="inp-text" type="number" v-model="dp.gravity"></p>
<input class="inp-range" type="range" min="0" max="20" step="1" v-model.number="dp.gravity">
</div>
<div class="d-item">
<label class="s-tit lb-ckb" for="inpau2s">
<input class="inp-text inp-checkbox" id="inpau2s" type="checkbox" v-model="dp.isPause">
暂停
</label>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.2.0/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vio-utils/utils.min.js"></script>
<script>
const particleArr = []
const vm = Vue.createApp({
setup(props, ctx) {
const { ref, reactive, onMounted, watch, computed } = Vue
const dp = reactive({
pCount: 50,
pSize: 12,
pColor: '#dddddd',
randColor: true,
pSpeed: 5,
pPos: 1,
friction: 0,
gravity: 4,
isPause: false,
})
const clientX = ref(0)
const clientY = ref(0)
const showSettingPanel = ref(false)
onMounted(() => {
document.addEventListener('contextmenu', (e) => {
showSettingPanel.value = true
clientX.value = e.clientX
clientY.value = e.clientY
e.preventDefault()
})
document.addEventListener('click', (e) => {
if (!document.querySelector('.setting-panel').contains(e.target)) {
showSettingPanel.value = false
}
})
})
watch(() => dp.pCount, (nVal, oVal) => {
dp.pCount = max(0, min(1000, nVal))
})
watch(() => dp.pSize, (nVal, oVal) => {
dp.pSize = max(0, min(50, nVal))
})
watch(() => dp.friction, (nVal, oVal) => {
dp.friction = max(0, min(1, nVal))
})
watch(() => dp.gravity, (nVal, oVal) => {
dp.gravity = max(0, min(20, nVal))
})
watch(() => dp.pSpeed, (nVal, oVal) => {
initPArr()
})
watch(() => dp.pPos, (nVal, oVal) => {
initPArr()
})
watch(() => dp.isPause, (nVal, oVal) => {
nVal ? noLoop() : loop()
})
const calcGravity = computed(() => {
return dp.gravity / 5
})
const stStyle = computed(() => {
return {
left: clientX.value + 'px',
top: clientY.value + 'px',
}
})
const getPosText = computed(() => {
const ob = {
0: '中',
1: '上',
2: '右',
3: '下',
4: '左',
}
return ob[dp.pPos]
})
return {
dp,
calcGravity,
stStyle,
getPosText,
showSettingPanel,
}
},
}).mount('#app')
function setup() {
createCanvas(window.innerWidth, window.innerHeight)
initPArr()
}
function draw() {
background('#34363f')
updatePArr()
particleArr.forEach((v, i) => {
v.draw(i)
v.update()
})
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight)
}
function initPArr() {
particleArr.length = 0
for (let i = 0; i < vm.dp.pCount; i++) {
particleArr.push(new Particle())
}
}
function updatePArr() {
let diff = vm.dp.pCount - particleArr.length
if (diff > 0) {
for (let i = 0; i < diff; i++) {
particleArr.push(new Particle())
}
} else {
particleArr.length = vm.dp.pCount
}
}
class Particle {
constructor() {
const r = randomGaussian
const obj = {
0: [r(width / 2), r(height / 2)],
1: [r(width / 2), r(30)],
2: [r(width - 30), r(height / 4)],
3: [r(width / 2), r(height - 30)],
4: [r(30), r(height / 4)],
}
let x = obj[vm.dp.pPos][0]
let y = obj[vm.dp.pPos][1]
let speed = vm.dp.pSpeed
this.isFreeze = false
this.speed = createVector(random(-speed, speed), random(-speed, speed))
this.pos = createVector(x, y)
this.size = vm.dp.pSize
this.color = vio.randColor()
}
draw(i) {
noStroke()
const x = max(this.size / 2, min(width - this.size / 2, this.pos.x))
const y = max(this.size / 2, min(height - this.size / 2, this.pos.y))
const pColor = vm.dp.randColor ? this.color : vm.dp.pColor
circle(x, y, this.size)
fill(pColor)
}
update() {
this.size = vm.dp.pSize
this.edgesDetection()
this.accSpeed()
if (this.isFreeze) {
this.speed.x = 0
this.speed.y = 0
return
}
this.pos.add(this.speed)
}
edgesDetection() {
let fc = vm.dp.friction
let isBottom = this.pos.y >= height - this.size / 2
if (this.pos.x <= this.size / 2 || this.pos.x >= width - this.size / 2) {
if (fc === 1 && vm.dp.gravity === 0) {
this.isFreeze = true
return
}
this.speed.x *= -(1 - fc / 2)
this.speed.y *= 1 - fc / 50
}
if (this.pos.y <= this.size / 2 || isBottom) {
if (fc === 1 && isBottom) {
this.isFreeze = true
return
}
if (this.speed.y > 0.1 || !vm.calcGravity) {
this.speed.x *= 1 - fc / 50
this.speed.y *= -(1 - fc / 2)
}
}
}
accSpeed() {
this.speed.y += vm.calcGravity
}
}
</script>
</body>
</html>