looptap:消磨时间的小游戏,把球停在有颜色区域
OneFile
源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="author" content="Vasanth.V" />
<meta name="theme-color" content="#FFFFFF" />
<link rel="apple-touch-icon" sizes="180x180" href="https://img.hellogithub.com/favicon/apple-touch-icon.png">
<link rel="android-chrome" sizes="192x192" href="https://img.hellogithub.com/favicon/android-chrome-192x192.png">
<link rel="android-chrome" sizes="512x512" href="https://img.hellogithub.com/favicon/android-chrome-512x512.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://img.hellogithub.com/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="https://img.hellogithub.com/favicon/favicon-16x16.png">
<link rel="icon" href="https://img.hellogithub.com/favicon/favicon.ico">
<title>Looptap - 消磨时间的小游戏</title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<style>
html,
body {
overscroll-behavior: none;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
margin: 0;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
background: #fbf9f6;
}
a {
color: inherit;
}
#canvas {
display: none;
height: 100vh;
width: 100%;
max-width: 640px;
overflow: hidden;
box-sizing: border-box;
margin: auto;
flex-direction: column;
justify-content: center;
}
#canvas svg {
width: 100%;
}
#about {
text-align: center;
font-size: 1.2rem;
line-height: 1.75;
padding: 0rem 2rem 1rem;
color: #2c3d51;
}
#about a {
font-weight: 500;
font-style: italic;
}
#ball {
transform: translateZ(0);
animation: rot 2000ms infinite linear;
animation-play-state: paused;
}
#ball.started {
animation-play-state: running;
}
#play {
cursor: pointer;
}
#finalscore,
#best {
font-size: 70%;
fill: #34495e;
}
#best {
font-style: italic;
font-size: 50%;
}
#tip {
font-weight: 300;
font-size: 18%;
font-style: italic;
padding: 0.5rem 2rem 0rem;
}
#shareBtn {
background: #1d9bf0;
color: #fff;
width: 150px;
text-align: center;
text-decoration: none;
border-radius: 2rem;
padding: 0.3rem 1.5rem;
margin: 1rem auto 0px;
font-size: 1.25rem;
}
#shareBtn.hide {
visibility: hidden;
}
@keyframes rot {
from {
transform: rotate(0deg) translate(40%) rotate(0deg);
}
to {
transform: rotate(360deg) translate(40%) rotate(-360deg);
}
}
</style>
</head>
<body>
<section id="canvas" v-bind:style="'display:flex;'">
<svg id="looptap" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect id="bg" x="0" cy="0" height="100" width="100" fill="none" />
<text x="50" y="50" dominant-baseline="middle" text-anchor="middle" class="score" id="score"
v-if="state === 'started'" v-html="score"></text>
<text x="50" y="32" text-anchor="middle" class="score" id="finalscore" v-if="state === 'stopped'"
v-html="score"></text>
<text x="50" y="70" text-anchor="middle" class="score" id="best" v-if="state === 'stopped'"
v-html="'Best: '+best"></text>
<g id="tip" v-if="state === 'init'">
<text x="50" y="68" text-anchor="middle" class="tip">
点击 ▶️ 开始 / 点击任何地方停止
</text>
<text x="50" y="74" text-anchor="middle" class="tip">
让球停在有颜色的区域就行!
</text>
</g>
<path id="arc" fill="none"
v-bind:stroke="colors[Math.floor(score / 10)] || colors[Math.floor((score - 270) / 10)] || '#bdc3c7'"
stroke-width="10" stroke-linejoin="round" stroke-linecap="round" v-bind:d="arcDValue" />
<circle id="ball" cx="50" cy="50" r="4" fill="#2C3D51" v-bind:class="state"
v-bind:style="'animation-duration: '+(2000 - taps * 40) + 'ms'" />
<polygon id="play" points="45,45 55,50 45,55" fill="#2C3D51" stroke="#2C3D51" stroke-width="5"
stroke-linejoin="round" stroke-linecap="round" v-if="state !== 'started'" v-on:click="startPlay" />
</svg>
<a id="shareBtn"
v-bind:href="'https://service.weibo.com/share/share.php?url=https%3A%2F%2Fhellogithub.com%2Fonfile%2Fcode%2Fcc759276aefe4bad87ac259940042581&title=%E6%88%91%E7%9A%84%E5%88%86%E6%95%B0%EF%BC%9A'+score+'%EF%BC%8CLooptap%20-%20%E6%B6%88%E7%A3%A8%E6%97%B6%E9%97%B4%E7%9A%84%E5%B0%8F%E6%B8%B8%E6%88%8F%E3%80%82'"
v-if="['stopped', 'started'].includes(state)" target="_blank"
v-bind:class="state === 'started' ? 'hide' : ''">
分享你的分数
</a>
<div id="about" v-if="state === 'init'">
源码:
<a href="https://github.com/vasanthv/looptap" target="_blank">Vasanth.V</a><br />
灵感来自 iOS 游戏 Coogyloop
</div>
</section>
</body>
<script>
const loopTapApp = new Vue({
el: "#canvas",
data: {
arc: [180, 270],
taps: 0,
score: 0,
best: window.localStorage.best || 0,
state: "init",
prevTapTime: 0,
colors: [
'#F26D22',
'#F1B21D',
'#ECE82F',
'#AED136',
'#7EC242',
'#63BC46',
'#65BD5D',
'#64C29A',
'#61C8D2',
'#2DAAE1',
'#456EB5',
'#4451A3',
'#6151A2'
]
},
computed: {
arcDValue: function () {
return this.describeArc(50, 50, 40, this.arc[0], this.arc[1]);
},
},
methods: {
polarToCartesian: function (centerX, centerY, radius, angleInDegrees) {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
return {
x: centerX + radius * Math.cos(angleInRadians),
y: centerY + radius * Math.sin(angleInRadians),
};
},
describeArc: function (x, y, radius, startAngle, endAngle) {
const start = this.polarToCartesian(x, y, radius, endAngle);
const end = this.polarToCartesian(x, y, radius, startAngle);
const arcFlag = endAngle - startAngle <= 180 ? "0" : "1";
const d = ["M", start.x, start.y, "A", radius, radius, 0, arcFlag, 0, end.x, end.y].join(" ");
return d;
},
getAngle: function (cx, cy, ex, ey) {
const dy = ey - cy;
const dx = ex - cx;
let theta = Math.atan2(dx, -dy);
theta *= 180 / Math.PI;
theta = theta < 0 ? theta + 360 : theta;
return theta;
},
getBallAngle: function () {
const bg = document.getElementById("bg").getBoundingClientRect();
const bgCenter = { x: bg.left + bg.width / 2, y: bg.top + bg.height / 2 };
const ball = document.getElementById("ball").getBoundingClientRect();
const ballCenter = { x: ball.left + ball.width / 2, y: ball.top + ball.height / 2 };
return this.getAngle(bgCenter.x, bgCenter.y, ballCenter.x, ballCenter.y);
},
setArc: function () {
const random = (i, j) => Math.floor(Math.random() * (j - i)) + i;
arc = [];
arc.push(random(0, 300));
arc.push(random(arc[0] + 10, arc[0] + 110));
arc[1] = arc[1] > 360 ? 360 : arc[1];
this.arc = arc;
},
startPlay: function () {
this.state = "started";
this.taps = 0;
this.score = 0;
this.prevTapTime = Date.now();
},
stopPlay: function () {
if (this.state === "started") {
this.state = "stopped";
if (this.score > this.best) window.localStorage.best = this.best = this.score;
}
},
tap: function (e) {
e.preventDefault();
e.stopPropagation();
if (this.state === "started") {
const ballAngle = this.getBallAngle();
// adding a 6 for better accuracy as the arc stroke extends beyond the angle.
if (ballAngle + 6 > this.arc[0] && ballAngle - 6 < this.arc[1]) {
const currentTapTime = Date.now();
const tapInterval = currentTapTime - this.prevTapTime;
this.taps++;
this.score = this.score + (tapInterval < 500 ? 5 : tapInterval < 1000 ? 2 : 1);
this.prevTapTime = currentTapTime;
this.setArc();
} else this.stopPlay();
}
},
},
});
if ("ontouchstart" in window) {
window.addEventListener("touchstart", loopTapApp.tap);
} else {
window.addEventListener("mousedown", loopTapApp.tap);
window.onkeypress = (e) => {
if (e.keyCode == 32) {
if (loopTapApp.state === "stopped") {
loopTapApp.startPlay();
} else {
loopTapApp.tap(e);
}
}
};
}
</script>
</html>