{"id":67,"date":"2025-10-29T13:08:02","date_gmt":"2025-10-29T05:08:02","guid":{"rendered":"http:\/\/photocaloric.com\/?p=67"},"modified":"2025-10-29T13:08:04","modified_gmt":"2025-10-29T05:08:04","slug":"flappy-bird","status":"publish","type":"post","link":"http:\/\/photocaloric.com\/index.php\/2025\/10\/29\/flappy-bird\/","title":{"rendered":"Flappy Bird"},"content":{"rendered":"\n<p>\u5199\u7684\u4e00\u4e2a\u5c0f\u6e38\u620f\uff0c\u8bf7\u73a9\u5b83\u653e\u677e\u653e\u677e\uff1a<\/p>\n\n\n\n<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\" \/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" \/>\n<title>Flappy Bird - \u8d85\u6162\u901f\u7248<\/title>\n<style>\n  :root{--bg:#70c5ce;--ground:#ded895;--pipe:#3cb043}\n  html,body{height:100%;margin:0}\n  body{display:flex;align-items:center;justify-content:center;background:linear-gradient(#a0e9ff,var(--bg));font-family:Inter,system-ui,Segoe UI,Roboto,\"Helvetica Neue\",Arial}\n  .wrap{width:420px;max-width:95vw;background:linear-gradient(#eaf9ff,#cfeff6);padding:14px;border-radius:12px;box-shadow:0 6px 30px rgba(10,30,40,0.2)}\n  h1{margin:0 0 8px;font-size:18px;text-align:center}\n  canvas{display:block;width:100%;height:auto;background:linear-gradient(#9be7ff,#61c2d8);border-radius:8px;box-shadow:inset 0 -20px 30px rgba(0,0,0,0.06)}\n  .controls{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:10px}\n  .left{font-size:13px;color:#034}\n  button{padding:8px 10px;border-radius:8px;border:0;background:#037dbe;color:#fff;cursor:pointer}\n  button.secondary{background:#8bbddf;color:#012}\n  .small{font-size:12px;color:#034}\n  .footer{font-size:12px;color:#034;margin-top:8px;text-align:center}\n<\/style>\n<\/head>\n<body>\n  <div class=\"wrap\">\n    <h1>Flappy Bird \u2014 \u8d85\u6162\u901f\u7248<\/h1>\n    <canvas id=\"game\" width=\"288\" height=\"512\" aria-label=\"Flappy Bird \u6e38\u620f\u753b\u5e03\"><\/canvas>\n    <div class=\"controls\">\n      <div class=\"left\">\n        <div class=\"small\">\u6309 \u7a7a\u683c \/ \u70b9\u51fb \/ \u89e6\u6478 \u4f7f\u5c0f\u9e1f\u4e0a\u5347<\/div>\n      <\/div>\n      <div>\n        <button id=\"startBtn\">\u5f00\u59cb \/ \u91cd\u65b0\u5f00\u59cb<\/button>\n        <button id=\"muteBtn\" class=\"secondary\">\u9759\u97f3<\/button>\n      <\/div>\n    <\/div>\n    <div class=\"footer\">\u901f\u5ea6\u8fdb\u4e00\u6b65\u653e\u6162\uff0c\u7b2c\u4e00\u6839\u7ba1\u9053\u5ef6\u8fdf\u51fa\u73b0\u3002<\/div>\n  <\/div>\n\n<script>\n(() => {\n  const canvas = document.getElementById('game');\n  const ctx = canvas.getContext('2d');\n  const W = canvas.width;\n  const H = canvas.height;\n\n  \/\/ \u8c03\u6574\u53c2\u6570\uff0c\u8fdb\u4e00\u6b65\u51cf\u901f\n  const GRAVITY = 0.25;\n  const JUMP_V = -6.5;\n  const PIPE_WIDTH = 52;\n  const PIPE_GAP = 150;\n  const PIPE_INTERVAL = 2500; \/\/ \u66f4\u6162\u751f\u6210\n  const FIRST_PIPE_DELAY = 2000; \/\/ \u6e38\u620f\u5f00\u59cb\u540e2\u79d2\u518d\u51fa\u73b0\u7b2c\u4e00\u4e2a\u7ba1\u9053\n  const GROUND_H = 112;\n\n  let bird = null;\n  let pipes = [];\n  let lastPipeAt = 0;\n  let score = 0;\n  let highScore = Number(localStorage.getItem('flap_high') || 0);\n  let running = false;\n  let gameOver = false;\n  let lastTime = 0;\n  let muted = false;\n  let startTime = 0;\n\n  const audioCtx = (window.AudioContext || window.webkitAudioContext) ? new (window.AudioContext || window.webkitAudioContext)() : null;\n  function beep(freq=440,dur=0.06, type='sine', vol=0.08){\n    if(muted || !audioCtx) return;\n    const o = audioCtx.createOscillator();\n    const g = audioCtx.createGain();\n    o.type = type; o.frequency.value = freq;\n    g.gain.value = vol;\n    o.connect(g); g.connect(audioCtx.destination);\n    o.start(); o.stop(audioCtx.currentTime + dur);\n  }\n\n  function createBird(){\n    return { x:60, y:H\/2-20, vy:0, w:34, h:24, rot:0 };\n  }\n\n  function reset(){\n    bird = createBird();\n    pipes = [];\n    lastPipeAt = 0;\n    score = 0;\n    gameOver = false;\n    running = true;\n    startTime = performance.now();\n    lastTime = performance.now();\n  }\n\n  function spawnPipe(at){\n    const minY = 80;\n    const maxY = H - GROUND_H - 80 - PIPE_GAP;\n    const top = Math.floor(minY + Math.random() * Math.max(0, maxY - minY));\n    pipes.push({x: W, topY: top, passed:false});\n    lastPipeAt = at;\n  }\n\n  function update(dt){\n    if(gameOver || !running) return;\n    bird.vy += GRAVITY * dt \/ (1000\/60);\n    bird.y += bird.vy * dt \/ (1000\/60);\n    bird.rot = Math.max(-0.6, Math.min(1.2, bird.vy \/ 10));\n\n    const speed = 0.7 * (1000\/60); \/\/ \u518d\u6b21\u51cf\u6162\u7ba1\u9053\u79fb\u52a8\u901f\u5ea6\n    for(const p of pipes){\n      p.x -= speed * dt \/ (1000\/60);\n    }\n    pipes = pipes.filter(p => p.x + PIPE_WIDTH > -20);\n\n    \/\/ \u5ef6\u8fdf\u751f\u6210\u7b2c\u4e00\u4e2a\u7ba1\u9053\n    if(performance.now() - startTime > FIRST_PIPE_DELAY && performance.now() - lastPipeAt > PIPE_INTERVAL){\n      spawnPipe(performance.now());\n    }\n\n    if(bird.y + bird.h\/2 >= H - GROUND_H){\n      bird.y = H - GROUND_H - bird.h\/2;\n      die();\n    }\n    if(bird.y - bird.h\/2 <= 0){\n      bird.y = bird.h\/2; bird.vy = 0;\n    }\n\n    for(const p of pipes){\n      const bx = bird.x - bird.w\/2;\n      const by = bird.y - bird.h\/2;\n      const bw = bird.w;\n      const bh = bird.h;\n      if(rectIntersect(bx,by,bw,bh, p.x, 0, PIPE_WIDTH, p.topY) || rectIntersect(bx,by,bw,bh, p.x, p.topY + PIPE_GAP, PIPE_WIDTH, H - GROUND_H - (p.topY + PIPE_GAP))){\n        die();\n      }\n      if(!p.passed && p.x + PIPE_WIDTH < bird.x - bird.w\/2){\n        p.passed = true; score += 1; beep(880,0.06,'sine',0.08);\n        if(score > highScore){ highScore = score; localStorage.setItem('flap_high', String(highScore)); }\n      }\n    }\n  }\n\n  function rectIntersect(x1,y1,w1,h1,x2,y2,w2,h2){\n    return !(x1 + w1 < x2 || x2 + w2 < x1 || y1 + h1 < y2 || y2 + h2 < y1);\n  }\n\n  function die(){\n    if(gameOver) return;\n    gameOver = true; running = false; beep(150,0.12,'square',0.16);\n  }\n\n  function draw(){\n    ctx.clearRect(0,0,W,H);\n    const g = ctx.createLinearGradient(0,0,0,H);\n    g.addColorStop(0,'#9be7ff'); g.addColorStop(1,'#61c2d8');\n    ctx.fillStyle = g; ctx.fillRect(0,0,W - 0,H - GROUND_H);\n\n    ctx.fillStyle = '#3cb043';\n    for(const p of pipes){\n      ctx.fillRect(Math.round(p.x), 0, PIPE_WIDTH, p.topY);\n      ctx.fillRect(Math.round(p.x), p.topY + PIPE_GAP, PIPE_WIDTH, H - GROUND_H - (p.topY + PIPE_GAP));\n      ctx.fillStyle = '#2e8b34';\n      ctx.fillRect(Math.round(p.x)-2, p.topY - 6, PIPE_WIDTH+4, 6);\n      ctx.fillRect(Math.round(p.x)-2, p.topY + PIPE_GAP, PIPE_WIDTH+4, 6);\n      ctx.fillStyle = '#3cb043';\n    }\n\n    ctx.fillStyle = '#ded895'; ctx.fillRect(0, H - GROUND_H, W, GROUND_H);\n\n    ctx.save();\n    ctx.translate(bird.x, bird.y);\n    ctx.rotate(bird.rot);\n    ctx.beginPath(); ctx.ellipse(0,0, bird.w\/2, bird.h\/2, 0, 0, Math.PI*2); ctx.fillStyle = '#ffdd55'; ctx.fill(); ctx.closePath();\n    ctx.beginPath(); ctx.fillStyle = '#000'; ctx.arc(6, -4, 2.8, 0, Math.PI*2); ctx.fill(); ctx.closePath();\n    ctx.beginPath(); ctx.moveTo(10,0); ctx.lineTo(18,4); ctx.lineTo(10,6); ctx.fillStyle = '#ff8c00'; ctx.fill(); ctx.closePath();\n    const flap = Math.max(-6, Math.min(8, -bird.vy * 1.6));\n    ctx.beginPath(); ctx.moveTo(-6, 0); ctx.quadraticCurveTo(-14 + flap, -10 + flap, -18, 0); ctx.fillStyle = '#f3c65b'; ctx.fill(); ctx.closePath();\n    ctx.restore();\n\n    ctx.font = '32px Arial'; ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.lineWidth = 4; ctx.strokeStyle = 'rgba(0,0,0,0.35)';\n    ctx.strokeText(String(score), W\/2, 80); ctx.fillText(String(score), W\/2, 80);\n\n    ctx.font = '14px Arial'; ctx.textAlign = 'right'; ctx.fillStyle = '#032'; ctx.fillText('High: ' + highScore, W - 8, 18);\n\n    if(gameOver){\n      ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(20,120,W-40,160);\n      ctx.fillStyle = '#fff'; ctx.font = '20px Arial'; ctx.textAlign = 'center';\n      ctx.fillText('\u6e38\u620f\u7ed3\u675f', W\/2, 170);\n      ctx.font = '16px Arial'; ctx.fillText('\u5f97\u5206\uff1a' + score, W\/2, 200);\n      ctx.fillText('\u70b9\u51fb\u5f00\u59cb\u6216\u6309 \u7a7a\u683c \u91cd\u65b0\u5f00\u59cb', W\/2, 235);\n    }\n  }\n\n  function loop(now){\n    const dt = Math.min(40, now - lastTime);\n    update(dt);\n    draw();\n    lastTime = now;\n    requestAnimationFrame(loop);\n  }\n\n  function flap(){\n    if(!running){ reset(); return; }\n    if(gameOver) { reset(); return; }\n    bird.vy = JUMP_V; bird.y += bird.vy; beep(660,0.04,'sine',0.08);\n  }\n\n  window.addEventListener('keydown', e=>{ if(e.code === 'Space'){ e.preventDefault(); flap(); } });\n  canvas.addEventListener('click', ()=> flap());\n  canvas.addEventListener('touchstart', (e)=>{ e.preventDefault(); flap(); }, {passive:false});\n\n  document.getElementById('startBtn').addEventListener('click', ()=>{ reset(); });\n  const muteBtn = document.getElementById('muteBtn');\n  muteBtn.addEventListener('click', ()=>{ muted = !muted; muteBtn.textContent = muted ? '\u5df2\u9759\u97f3' : '\u9759\u97f3'; });\n\n  reset();\n  requestAnimationFrame(loop);\n\n  function resumeAudio(){ if(audioCtx && audioCtx.state === 'suspended') audioCtx.resume(); window.removeEventListener('click', resumeAudio); window.removeEventListener('keydown', resumeAudio); }\n  window.addEventListener('click', resumeAudio); window.addEventListener('keydown', resumeAudio);\n})();\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>\u5199\u7684\u4e00\u4e2a\u5c0f\u6e38\u620f\uff0c\u8bf7\u73a9\u5b83\u653e\u677e\u653e\u677e\uff1a Flappy Bird &#8211; \u8d85\u6162\u901f\u7248 Flappy Bird \u2014 \u8d85\u6162\u901f\u7248 \u6309 \u7a7a\u683c \/ \u70b9\u51fb  &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"emotion":"","emotion_color":"","title_style":"","license":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-67","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/posts\/67","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/comments?post=67"}],"version-history":[{"count":1,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/posts\/67\/revisions"}],"predecessor-version":[{"id":68,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/posts\/67\/revisions\/68"}],"wp:attachment":[{"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/media?parent=67"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/categories?post=67"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/photocaloric.com\/index.php\/wp-json\/wp\/v2\/tags?post=67"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}