`
mycream
  • 浏览: 55294 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

favicon 小游戏探索

 
阅读更多

老早之前在 cnBeta 上看到一款用 favicon 做成的游戏(我是传送门)。16*16的显示窗里放下一个飞机游戏,创意不错。游戏地址:http://www.p01.org/defender_of_the_favicon/

今天抽时间,把游戏弄了出来,主要是使用 canvas 画板绘制游戏,再复制到 favicon 中。原理很是简单。下面把简化版放上,有兴趣的童鞋可以自行研究下。(相对完整的版本见后面的附件)

是了,遇到canvas加载问题,那就请开 http-server。因为画板在本地运行有个图片绘制权限问题。

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>DEFENDER OF THE FAVICON</title>
  <link rel="shortcut icon" type="image/ico" href="favicon.ico"></link>
</head>
<body>
  <!-- 游戏素材 -->
  <img id="title" crossorigin="anonymous" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADcAAAAQCAYAAACycufIAAABe0lEQVRIS91W0RLDIAjT//9odzjwYgSLbntZX3q1FQhJqLXA1Vpp+FxrqfLM67Im727XLS7G9uLd5jcMvXi7bovlhmTicOGfNMsjpBPgApNV5XBKCq2oBZg7XdekGHvJc5ofNGeNW8EBsA59EqpV1btyLUvs9JAQyjxqLm8M6vsIXCShGy964EJGM+Ck66rIZWBspQLETb46laXT8S0gVlNUB4CXeOE0VPjP0ptGUiBjnq62h3ziDaJe70Z+TYxjceg7t/hdwEV6IIPot8F7OkvmZm9wkPx2NpiAL+D8kfEeGIl3pu8xdQ/2YPxsPsd2o06OkQLwFHD6Vx6A60zr9z8B543lf1njcfDupPhAvWQmxyMTe8s8wXfz2sSs2n/4jnK5sbSmaI/NCLbICq6V5iXw2BTgeIzK7MvssW/4bk2NmskEpMB5DGCiiMlH5oJGfhWcSTHbkTEZv8DcdEJxLIE1ZZjrNlJLpTzHdGfBRcxFUvbUgD7L+BTt8wK4FusW3zOi6QAAAABJRU5ErkJggg==" />
  <img id="city" crossorigin="anonymous" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARAAAAAgCAYAAADaBycMAAAEx0lEQVR4Xu1czWoUQRCuRoMEJBAw7iTmJjkEES8iJNdoEESY5AGi4gPkmlwEQTTXvIIPkPgUySKoIOol4E0yC4EFrx4ic8hud+9sV1dXz+78VG6dqvrqq6+qK62HUQBwCRF++v0+zM/Pj0XC7BEoOCE4+TmxZdfFwW9qXVeaNKG+qtegYi0QziBXNRZr3tnZGaysrLDo+2BgPFgEIgbbtbhq86k7IrVgqEnynEQufZZi5JMFEjxafoEnJyewvr7u52x5cWKDEgJAjKEKzR0aF0OnOtYdqlfMuIkukBiNjlk8F+v4+Bi2trYGMPZZx3fZuDxc8eM0t/k0rTdFmrShxnGzUFbtrAUyrUtBvXCxeMbCofJvu7/oXs0JyPviXCCHh4ewu7tLZh8a50pUBqaer2x8u7Yy88XA9sHw8SEPTwsCqLrZ/tT40Dn3ycN6gcTo9d7eHhwcHAyg7DMlByWW4qtzCI1z1VEGJkW3KvliWmB2n1p8MHx8fHI13WfqC6TpAlPra9Lgpjv78OnjB6oEhn+T9GAJUdHgwgWyvb0NR0dHFaVcHq1p1x0rfyyc8pQuRq4r73E6cerhxHL7RsntfIGsra3B6ekpl493vJ5v0rm9SU7Jsel6UOuz/anxehspsRTf0By+IxbKJSa+/BPGV03xm5oCZV8UrLBp58f4FdmTJIEsy0JCSTEjCwRLjNl9svtg+Pj45BIfugKT1H4SufQck8hHV7w6EZg+tr1aL5CZOYB/f0fVtH8/zq86feAzkRr5GsZCoPSC4huLny9OCdyqtUB8hRA/UUAUqIQCskAq0QYhIQrUUwFZIPXsm7AWBSqhgCyQSrRBSIgC9VRAgboe5YNC9SxfWIsCogBHAQXXZmWBcBSUWFGgxQoomJmTBdLiAZDSRQGOAgpuLMgC4SgosaJAixVQMLssC6TFAyCliwIcBRTcvCsLhKOgxIoCLVZAwdw9WSAtHgApXRTgKKDSnf3BAsl6F9D9/JWDJ7GigChQIwXS55sDtiH3X6Uv3lxm2fkApPvlV43KF6qigCjAUSB9tgGc+z/6Avn2m8NHYkUBUaBGCqRPH5kvEOL9H3mBJJ1bBqB97n7/45QnffLAJIT4Y1rbeFQ+GD7XzuVHjcf8qf2L3S+untR44U9VzPRPNx8aLxDq/Kj05dvh/4FkGSSdBRMwWTSfOD8uDAbp4/vDhZGdQxLgfwWQP6W6Bfj6E4uKX4TnykdtR16/i1/OV89XdKbWh+Xj4nH0KZoHrH5KPmzebH3t/tv9xfhi8RgeFl80Py69MDxsfkfqXVzW5pN+/1X66t2l/umzfIEMC8gg/wKRfu7+7JsLZGPV+HSa7V90NvJZn14rwnfx8cG34+38rvpGBmRj1amPDx9Xfh+9qXpQ/bn8XP21v2hln+3+F+mP8dPtIXjUeNd8+eTH5pMyr6k1n+h9Xbxj3l/i/Vfp6/faC6QHye38BTL8lmJRw7GB5MZTBaUMlM8F59aH8aHWR/XH9Mfs1HyYXmXbqXrH5sPNT43H/DG7Uf/SkvYHkX7/RxdI0jEBC85Z1hv4JEkH7POQYA9yu33G4ql4GD7XHpsPFY/qj+mP2an5qP2M7Y/1N3Y+TB9qPip/zB+zY/wp8/EfaBbMJmzqBoAAAAAASUVORK5CYII=" />
  <img id="player" crossorigin="anonymous" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAgCAYAAAAxOQljAAAAdklEQVQoU7WS0Q2AIAxErzMYHUx30DFkDd3BxQzOcKaQECUIfMiFn/YRXqAIPiLaJ0nZASziak0ArnjAsCM+MQ+SDtuP7M7DCxs5uIIwbOsgCF3/3CO87EZyBkQTN53rBaKBVA6q5LiGyX+GVCpByQEj7R3IOG4RCUntPt+ZagAAAABJRU5ErkJggg==" />
  <img id="digits" crossorigin="anonymous" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAUCAYAAADPym6aAAABIUlEQVRIS+1Wiw7CMAgsf7Z92bYv2/4MU5XmJAzwkaYaTRbtepTj4OaIS+GiPlQK4f26rhCN1Tg8RmKiOA+neXnrRhhJaeKYDIlFZCWxCBCRjs7zhEwXkumc7ogV43U/KtSNRbW0ct6ePtQjnVU6i7NG7MELAvBmP5rbZ1XN+irC3UzMzERE8r0sC6/rWuq1bVu7j7gaV2Oiwnrtm4Xows7WvUhm8rxcyLAdaf64jxiuz0Yto1QvTOuIeAK9IiOFPpLfQ3ZEyM3zXI7jIOkAqinGn6aJ930fyuhXUZGsZWpRXu/1GplsHvPxaT1mccyyh/fEmR1BH6DptVd6Eo1ytUKq4uIRy9zy5/cVHomqHnl/mFeMd0X6F/Kugp+O/5mOXADYeR4aGRLmGgAAAABJRU5ErkJggg==" />
  <img id="sprites" crossorigin="anonymous" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAj0lEQVQ4T81TQQ6AIAxrPytvws/WbAgiIiHBgwsJZLBR2kIAkCSbSdLmHG/5+owXLDXwYhKMgrYLheW5Awq0Gx7oMoob5Bra7Lo8wW5DaDgAEjc++pE2YiKx22AA/2w+C3aEYKHHdzKaVLWZijdMykreFuw3Mi5QAAoSzYpNCNBI/+JEt2zzifL/6OX/x8EBupROEXY6Qx8AAAAASUVORK5CYII=" />
  <!-- 画板,通过画板绘制游戏,再复制到 favicon 中 -->
  <canvas id="render" width="16" height="16"></canvas>
  <br/>
  <h3>操作</h3>
  <code>N</code>:开始/射击<br/>
  <code>WASD</code> 或方向键: 方向控制
  <script>
  // 重定义 getElementById 方法,为方便获取页面元素
  function $(id) {
    return document.getElementById(id)
  }
  Function.prototype.bind || (Function.prototype.bind = function(what) {
    var _method = this;
    return function(){ return _method.apply(what, arguments) };
  });
  var game = new (function() {
    var x = 0,
      y = 8,
      shield = 16,
      score = 0,
      wave = 0,
      goLeft = true,
      speed = 0,
      state = '',
      stateBefore = '',
      update = {},
      now = 0,
      then = 0,
      keyDown ={},
      keyUp ={},
      keyMapping =
      {
        38: 'up',
        87: 'up',
        40: 'down',
        83: 'down',
        37: 'left',
        65: 'left',
        39: 'right',
        68: 'right',
        68: 'right',
        78: 'fire',
        13: 'select',
        0:  '** dummy **'
      },
      fireDelay = 0,
      fireRate = 3,
      enemies = [],
      humanoids = [],
      warmupDuration = 1500,
      frame = 0,
      ctx = null,
      useIcon = true,
      stupidBrowser = false;
    /*
     *  initialize
     */
    window.addEventListener
    (
      'load',
      (function(/*onload*/) {
        // give favicon an id
        var icons = document.getElementsByTagName('link');
        for (var i = 0; i < icons.length; i++) {
          var icon = icons[i];
          if (icon.getAttribute('rel') == 'shortcut icon') {
            icon.setAttribute('id', 'favicon');
            i = icons.length;
          }
        }
        // keyboard
        for (var i in keyMapping) {
          keyDown[keyMapping[i]] = keyUp[keyMapping[i]] = 0;
        }
        onkeyup = onkeydown = handleKeyEvents.bind(this);
        //  blur/focus = pause/resume
        onblur = onfocus = (function(event){
          var event = event || window.event;
          if (state == 'play'|| state == 'pause')
            setState(event.type.toLowerCase() == 'blur' ? 'pause' : stateBefore);
        }).bind(this);
        //  get rendering context
        ctx = $('render').getContext('2d');
        //  is this a stupidBrowser that do not support toDataURL ?
        if ('function' != typeof(ctx.canvas.toDataURL)) {
          stupidBrowser = true;
          ctx.canvas.className = 'stupidBrowser';
          toggleIconUse();
        }
        //  start with the 'title' state
        setState('title');
        // there we go
        main();
      }).bind(this),
      false
   );
    /*
     *  handleKeyEvents
     */
    function handleKeyEvents(event) {
      var event = event || window.event,
        type = event.type.toLowerCase(),
        keyCode = keyMapping[event.keyCode] || 'unknown';
      keyDown[keyCode] = type == 'keydown' ? 1 : 0;
      keyUp[keyCode] = type == 'keyup' ? 1 : 0;
      if (keyCode != 'unknown' && state == 'play' && event.preventDefault) {
        event.preventDefault();
      }
    }
    /*
     *  toggleIconUse
     */
    function toggleIconUse() {
      useIcon =! useIcon;
      ctx.canvas.setAttribute('style', 'visibility:' + (useIcon ? 'hidden' : 'visible'));
      if (!useIcon) {
        document.getElementById('favicon').setAttribute('href', 'favicon.png');
      }
    }
    /*
     *  setState
     */
    function setState(newState) {
      if ('function' !== typeof(update[newState])) {
        throw new Error('state "' + newState + '"not supported');
      }
      then = new Date().getTime();
      stateBefore = state;
      state = newState;
    }
    /*
     *  initializeWave
     */
    function initializeWave(newWave) {
      wave = newWave || 1;
      if (wave == 1) {
        score = 0;
      }
      //  reset player
      x = 0;
      y = 8;
      shield = 16;
      //  enemies
      enemies = [];
      var len = wave + 10;
      for (var i = 0; i < len; i++) {
        enemies[i] =
        {
          x: Math.random()*255&255,
          y: -8,
          type: (i < 10 ? 0 : (1 + (i & 1))),  // 10 Landers then it's half Baiter half Bomber
          isAlive: true
        };
      }
      //  humanoids
      for (var i = 0; i < 10; i++) {
        humanoids[i] =
        {
          x: Math.random() * 255 & 255,
          y: Math.random() * 13 & 15,
          isGrabbed: false
        };
      }
      //  play!
      setState('play');
    }
    /*
     *  update.title
     */
    update.title = function() {
      ctx.drawImage($('title'), 0,0, 55,16, Math.round(39*Math.cos((now/1337)%6.283)-16),0, 55,16);
      if (keyUp.fire && now > 500) {
        initializeWave(1);
      }
    }
    /*
     *  gameover
     */
    update.gameover = function() {
      // ...
      if (now < warmupDuration) {
        now = warmupDuration - now;
        update.play();
        ctx.globalCompositeOperation = 'lighter';
        ctx.fillStyle = '#f00';
        ctx.globalAlpha = now/warmupDuration;
        ctx.fillRect(0,0,16,16);
        ctx.globalCompositeOperation = 'source-over';
        ctx.globalAlpha = 1;
        now = warmupDuration - now;
      }
      else
      {
        var yOfs = -Math.round(4 * Math.sin(((now - warmupDuration) / 241) % 6.283));
        ctx.drawImage($('digits'), 0, (state == 'gameover' ? 10 : 5), 16, 5, 0, yOfs + 2, 16, 5);
        ctx.globalAlpha = now & 256 ? 1 : .5;
        ctx.drawImage($('digits'), Math.floor(wave / 10 % 10) * 5, 0, 5, 5, 2, yOfs + 9, 5, 5);
        ctx.drawImage($('digits'), Math.floor(wave % 10) * 5, 0, 5, 5, 9, yOfs + 9, 5, 5);
        ctx.globalAlpha = 1;
      }
      if (keyUp.fire && now > 2000) {
        setState('title');
      }
    }
    /*
     *  update.pause
     */
    update.pause = function() {
      ctx.drawImage($('digits'), 0,5,16,5, 0, 2,16,5);
      ctx.globalAlpha= now&256?1:.5;
      ctx.drawImage($('digits'), Math.floor(wave/10%10)*5,0,5,5, 2, 9,5,5);
      ctx.drawImage($('digits'), Math.floor(wave%10)*5,0,5,5, 9, 9,5,5);
      ctx.globalAlpha= 1;
    }
    /*
     *  update.play
     */
    update.play = function() {
      var isStarting = now < warmupDuration;
      var isTouched = false;
      var yOfs = isStarting?Math.round((stateBefore=='play'?48:32)*Math.cos(Math.PI/2*now/warmupDuration)):0;
      if (!isStarting && shield) {
        // update player's position
        speed = Math.max(-3, Math.min(3, speed-(!speed?0:speed/Math.abs(speed)/4)+keyDown.left-keyDown.right));
        x = (x+256-Math.round(speed))&255;
        goLeft = speed?speed>0:goLeft;
        y = Math.max(0, Math.min(14, y+keyDown.down-keyDown.up));
      }
      var xxPlayer = Math.round(5+speed*4/3);
      //  draw city
      ctx.drawImage($('city'), -((x+16384)&255), yOfs-16);
      //  start transition
      if (isStarting) {
        ctx.drawImage($('digits'), 0,(state=='gameover'?10:5),16,5, 0, yOfs-(state=='gameover'?48:32)+2,16,5);
        ctx.globalAlpha= now&256?1:.5;
        ctx.drawImage($('digits'), Math.floor(wave/10%10)*5,0,5,5, 2, yOfs-(state=='gameover'?48:32)+9,5,5);
        ctx.drawImage($('digits'), Math.floor(wave%10)*5,0,5,5, 9, yOfs-(state=='gameover'?48:32)+9,5,5);
        ctx.globalAlpha= 1;
      }
      //  playing
      else
      {
        //  fire
        if (fireDelay) {
          fireDelay--;
        }
        else if (keyUp.fire && shield) {
          fireDelay = fireRate;
          var fireX = ([0,Math.round(5+speed+3),16]).splice(goLeft?0:1,2);
          ctx.fillStyle = (['#ff0','#0f0','#0ff'])[frame%3];
          ctx.fillRect(fireX[0], y+1, fireX[1]-fireX[0], 1 );
        }
        //  enemies
        var enemiesAlive = 0;
        for (var i = 0, enemy; enemy = enemies[i]; i++) {
          if (!enemy.isAlive) {
            continue;
          }
          enemiesAlive++;
          var iCanHasThrust = Math.random()*4&3;
          //  y
          if (iCanHasThrust== 0) {
            var s = y-enemy.y+Math.random()*2-1
            //  landers ?
            if (!enemy.type) {
              if (humanoids[i].isGrabbed) {
                //  abduct humanoid
                s = -1;
              }
              else
              {
                s = enemy.x== humanoids[i].x?1:Math.random()-.5;
              }
            }
            if ((s<0 && enemy.y>(enemy.type?0:-5)) || (s>0 && enemy.y<(enemy.type?13:11))) {
              enemy.y += s/Math.abs(s);
              if (!enemy.type) {
                if (enemy.y== 11 && enemy.x== humanoids[i].x && humanoids[i].y== 14) {
                  humanoids[i].isGrabbed = true;
                }
                else if (enemy.y==-5 && humanoids[i].isGrabbed) {
                  humanoids[i].y = 16;
                  humanoids[i].isGrabbed = false;
                }
              }
            }
          }
          //  x
          else if (iCanHasThrust== 2) {
            var s = (enemy.type?x+Math.random()-.5:humanoids[i].x+Math.random()-.5)-enemy.x;
            if (s) {
              enemy.x = (enemy.x+256+s/Math.abs(s))&255;
            }
          }
          //  lander grabbing a humanoid ?
          if (!enemy.type && humanoids[i].isGrabbed) {
            humanoids[i].x = enemy.x;
            humanoids[i].y = enemy.y+3;
          }
          //  draw enemy
          var xx = (Math.round(256+16+enemy.x-x)&255)-8;
          ctx.drawImage($('sprites'), 6*((i+frame)&1),enemy.type*4,4,3, xx-2, Math.round(enemy.y)+yOfs,4,3 );
          //  aligned with the player ?
          if (shield && Math.abs(enemy.y-y)<(enemy.type?3:2)) {
            //  shoot ?
            if (fireDelay== fireRate && xx-2<fireX[1] && xx+2>fireX[0]) {
              enemy.isAlive = false;
              if (!enemy.type && humanoids[i].isGrabbed) {
                humanoids[i].isGrabbed = false;
              }
            }
            //  crash on player ?
            else if (xx-2+(enemy.type&1)<xxPlayer+5 && xx+2-(enemy.type&1)>xxPlayer) {
              isTouched = true;
            }
          }
        }
        var lostHumanoids = 0;
        //  humanoids' "logic"
        for (var i = 0, humanoid; humanoid = humanoids[i]; i++) {
          //  abducted ?
          if (humanoid.y== 16) {
            humanoid.x = Math.random()*255&255; // toss the lander around
            lostHumanoids++;
          }
          //  free fall
          else if (!humanoid.isGrabbed && humanoid.y<14) {
            humanoid.y++;
          }
        }
      }
      //  draw humanoids
      for (var i = 0, humanoid; humanoid = humanoids[i]; i++) {
        var xx = (Math.round(256+16+humanoid.x-x)&255)-8;
        ctx.drawImage($('sprites'), 6*((i+frame)&1),12,1,2, xx-(humanoid.isGrabbed?(frame+i)&1:0), Math.round(humanoid.y)+yOfs,1,2 );
      }
      //  draw player
      if (shield) {
        ctx.drawImage($('player'), 0,(goLeft?16:0)+Math.round(Math.abs(speed))*4, 6,4, xxPlayer,y-1+yOfs, 6,4);
        if (isTouched) {
          ctx.globalCompositeOperation = 'lighter';
          ctx.fillStyle = '#f00';
          ctx.fillRect(0,0,16,16);
          ctx.globalCompositeOperation = 'source-over';
          shield--;
          if (lostHumanoids== 10 || !shield) {
            setState('gameover');
          }
        }
        //  no more enemies ? -> next wave
        if (!isStarting && !enemiesAlive) {
          initializeWave(wave+1);
        }
      }
    }
    /*
     *  main
     */
    function main() {
      now = new Date().getTime()-then;
      //  toggleIconUse ?
      if (!stupidBrowser && keyUp.select) {
        toggleIconUse();
      }
      //  clear
      ctx.globalCompositeOperation = 'source-over';
      ctx.fillStyle = '#000';
      ctx.fillRect(0,0,16,16);
      // actual update
      if (update[state]) {
        update[state]();
      }
      // set favicon
      if (!stupidBrowser && useIcon) {
        var icon=$('favicon');
        (newIcon = icon.cloneNode(true)).setAttribute('href',ctx.canvas.toDataURL());
        icon.parentNode.replaceChild(newIcon,icon);
      }
      //  reset keyUp
      for (var i in keyMapping) {
        keyUp[keyMapping[i]]= 0;
      }
      //  update
      frame++;
      setTimeout(main.bind(this), 50);
    }
  })();
  </script>
</body>
</html>
分享到:
评论

相关推荐

    太空飞船探索网页模板

    6. icons文件夹:图标资源,如矢量图标或favicon.ico,用作浏览器标签的图标。 7. 其他可能的文件如header.html、footer.html、about.html等,用于页面部分的复用或特定内容的展示。 这款模板的使用者需要有一定的...

    ballcup:用RN开发游戏的示例, react-native-game-engine + Matter + Sensors

    很多情况下需要我们在app中加入游戏性的元素,比如给小朋友用的学习类软件。做成游戏的形式会比较讨巧。 本文目的,探索用RN开发游戏的可能性,本文所做的尝试离开发一个完整的游戏还差的比较远 准备: 安装RN开发...

    经典的ico图标各种尺寸

    ico图标是一种特殊的图像格式,主要用于网站的favicon,即浏览器地址栏前面的小图标,也可用于桌面快捷方式或系统文件的图标。这些ico图标包含了多种尺寸,以适应不同的显示需求,如小到16x16像素的低分辨率,大到...

    图标设计的基础知识.pdf

    - Favicon是网站在浏览器标签中的小图标,用于快速识别。 2. **基于视觉特征的图标类型**: - 字符图标由字母、数字和图形组成,简洁且具有高识别度。 - 扁平和半扁平图标注重清晰、直观的信息传达,避免过多...

    shaders:着色器游乐场

    它允许用户通过直观的方式探索和理解如何使用着色器来创建复杂的视觉效果。 着色器是一种小程序,它们运行在GPU(图形处理单元)上,负责计算屏幕上每个像素的颜色和外观。在WebGL环境中,着色器主要分为两种类型:...

    zerg-overmind.com:Github页面

    这个页面的命名“Zerg-Overmind”可能来源于《星际争霸》游戏中的虫族领袖,暗示了开发者可能对游戏或策略有浓厚的兴趣,并将其融入到他们的编程文化中。 提到的标签“CSS”是Cascading Style Sheets的缩写,是用于...

    ZombieApocalypse:学习HTML,CSS和JavaScript

    6. `icons`:小图标文件,如favicon,用于浏览器标签页。 7. `scripts`或`js`:可能有额外的JavaScript文件,用于更复杂的逻辑或模块化代码。 通过实践这样的项目,初学者不仅能掌握HTML、CSS和JavaScript的基础...

    gamer234emp.ga

    6. **图标文件**:通常包括 favicon.ico,这是在浏览器标签页上显示的小图标。 7. **JSON 或其他配置文件**:这些文件可能包含网站设置、API 配置或其他数据。 8. **构建脚本和工具**:如果是开发环境,可能会有...

    bedrockminers.github.io:服务器网站,就是这样

    6. **图标文件**(如favicon.ico):浏览器地址栏和书签中显示的小图标。 7. **其他HTML文件**:除了主页外,网站可能还包括其他页面,如关于、服务、联系等页面。 在CSS方面,开发者可能使用了各种技术来增强网站...

    FinalProject:历史实验室

    "历史实验室"可能寓意用户可以通过互动体验来探索历史,就像在实验室里进行实验一样,提供一种新颖的学习方式。这样的项目可能包含了丰富的历史数据、交互式时间线、多媒体资源以及教育游戏等,旨在增强学习者对历史...

Global site tag (gtag.js) - Google Analytics