微件:GGLScratchGame:修订间差异

来自Limbo Wiki Mirror
Gaoice留言 | 贡献
无编辑摘要
Gaoice留言 | 贡献
无编辑摘要
标签(旧)WikiEditor
第1行: 第1行:
<div class="scratch-lottery-root" data-config="">
<noinclude>
  <div class="sl-debug"></div>
这是一个刮刮乐 Widget
</noinclude>


  <div class="sl-header">
<div class="scratch-wrapper">
    <div class="sl-info">
  <!-- 背景 -->
      <span class="sl-ticket-count"></span>
  <img class="scratch-bg"
      <span class="sl-ticket-index"></span>
      src="https://wm.gaoice.run/images/thumb/b/b6/%E5%9B%BE%E7%89%871.png/180px-%E5%9B%BE%E7%89%871.png"
    </div>
      alt="lottery bg">
    <button class="sl-open-btn">来一张彩票</button>
  </div>


   <div class="sl-stage">
   <!-- 刮开后 -->
    <div class="sl-canvas">
  <img class="scratch-reveal"
      <img class="sl-bg" />
      src="https://wm.gaoice.run/images/thumb/4/4a/%E5%88%AE%E5%BC%80%E5%90%8E.jpg/180px-%E5%88%AE%E5%BC%80%E5%90%8E.jpg"
      <div class="sl-area"></div>
      alt="reveal">
    </div>
  </div>


   <div class="sl-controls">
   <!-- canvas 覆盖层 -->
    <button class="sl-mascot">(◕‿◕) …</button>
  <canvas class="scratch-canvas"></canvas>
    <button class="sl-scan">扫描结果</button>
  </div>
</div>
</div>


<style>
<style>
.scratch-lottery-root {
.scratch-wrapper {
   max-width: 520px;
   position: relative;
   margin: 2em auto;
   width: 360px;
  font-family: system-ui, sans-serif;
   height: 360px;
   user-select: none;
}
}
.sl-header {
 
  display: flex;
.scratch-bg,
  justify-content: space-between;
.scratch-reveal {
  align-items: center;
  margin-bottom: .5em;
}
.sl-open-btn { padding: .4em .8em; }
.sl-stage { position: relative; }
.sl-canvas { position: relative; width: 100%; }
.sl-bg { width: 100%; display: block; }
.sl-area { position: absolute; }
.sl-tile {
   position: absolute;
   position: absolute;
   width: 169px;
   inset: 0;
  height: 169px;
  cursor: pointer;
  outline: none;
}
.sl-cover img,
.sl-reveal img {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   object-fit: cover;
   pointer-events: none;
}
}
.sl-bubble {
 
.scratch-canvas {
   position: absolute;
   position: absolute;
   background: #fff;
   inset: 0;
  border: 1px solid #000;
   width: 100%;
  padding: .4em .6em;
   height: 100%;
  font-size: 13px;
   cursor: pointer;
  z-index: 50;
}
.sl-controls {
  display: flex;
  margin-top: .5em;
}
.sl-controls button {
  flex: 1;
  padding: .6em;
}
.sl-scan.highlight {
  background: #ffe600;
  animation: pulse 1s infinite;
}
@keyframes pulse {
  from { box-shadow: 0 0 0 rgba(255,230,0,.8); }
  to { box-shadow: 0 0 12px rgba(255,230,0,0); }
}
.sl-invert {
  filter: invert(1) hue-rotate(180deg);
}
.sl-debug {
   font-size: 11px;
   color: #666;
   margin-bottom: .5em;
}
}
</style>
</style>


<script>
<script type="text/javascript">
(function () {
(function () {
   const root = document.currentScript.parentElement;
   var wrapper = document.currentScript.parentElement;
   const cfg = JSON.parse(root.dataset.config || '{}');
   var canvas = wrapper.querySelector('.scratch-canvas');
  var ctx = canvas.getContext('2d');


   const bg = root.querySelector('.sl-bg');
   var size = 360;
   const area = root.querySelector('.sl-area');
   canvas.width = size;
  const mascot = root.querySelector('.sl-mascot');
   canvas.height = size;
  const scanBtn = root.querySelector('.sl-scan');
   const openBtn = root.querySelector('.sl-open-btn');
  const debugBox = root.querySelector('.sl-debug');


   const tickets = cfg.tickets || [];
   var coverImg = new Image();
   const states = cfg.states || {};
   coverImg.crossOrigin = 'anonymous';
  let ticketIndex = 0;
   coverImg.src =
   let playerTickets = cfg.initialTickets || 3;
    'https://wm.gaoice.run/images/thumb/5/5a/%E5%88%AE%E5%BC%80%E5%89%8D.png/180px-%E5%88%AE%E5%BC%80%E5%89%8D.png';
  let currentTicket = null;
  let revealed = new Set();


   bg.src = cfg.config.bg;
   coverImg.onload = function () {
    ctx.drawImage(coverImg, 0, 0, size, size);
  };


   function layout() {
   var scratching = false;
    const scale = bg.clientWidth / 1075;
    area.style.left = (cfg.config.area.x * scale) + 'px';
    area.style.top = (cfg.config.area.y * scale) + 'px';
    area.style.width = (cfg.config.area.width * scale) + 'px';
    area.style.height = (cfg.config.area.height * scale) + 'px';
  }
  bg.onload = layout;
  window.addEventListener('resize', layout);
 
  function updateInfo() {
    root.querySelector('.sl-ticket-count').textContent = '🎟️ 票数:' + playerTickets;
    root.querySelector('.sl-ticket-index').textContent =
      currentTicket ? '当前票:' + currentTicket.id : '当前票:-';


    if (cfg.debug) {
  function getPos(e) {
      debugBox.textContent =
    var rect = canvas.getBoundingClientRect();
        'ticketIndex=' + ticketIndex +
    var p = e.touches ? e.touches[0] : e;
        ' revealed=' + revealed.size +
    return {
        ' playerTickets=' + playerTickets;
      x: p.clientX - rect.left,
     }
      y: p.clientY - rect.top
     };
   }
   }


   function openTicket() {
   function start(e) {
     if (playerTickets <= 0 || ticketIndex >= tickets.length) return;
     scratching = true;
    currentTicket = tickets[ticketIndex++];
     e.preventDefault();
    playerTickets--;
    revealed.clear();
     scanBtn.classList.remove('highlight');
    mascot.style.visibility = 'visible';
    renderTiles();
    updateInfo();
   }
   }


   function renderTiles() {
   function move(e) {
    area.innerHTML = '';
     if (!scratching) return;
    currentTicket.outcomes.forEach((stateId, i) => {
     e.preventDefault();
      const tile = document.createElement('div');
     var p = getPos(e);
      tile.className = 'sl-tile';
     ctx.globalCompositeOperation = 'destination-out';
      tile.tabIndex = 0;
     ctx.beginPath();
      tile.setAttribute('aria-label', '刮奖格子 ' + (i + 1));
     ctx.arc(p.x, p.y, 18, 0, Math.PI * 2);
 
     ctx.fill();
      const col = i % cfg.config.cols;
      const row = Math.floor(i / cfg.config.cols);
      tile.style.left = (col * cfg.config.tileSize) + 'px';
      tile.style.top = (row * cfg.config.tileSize) + 'px';
 
      const cover = document.createElement('div');
      cover.className = 'sl-cover';
 
      const coverImg = document.createElement('img');
      coverImg.src = cfg.config.cover;
      cover.appendChild(coverImg);
      tile.appendChild(cover);
 
      tile.onclick = () => revealTile(i, tile);
      tile.onkeydown = e => {
        if (e.key === 'Enter' || e.key === ' ') revealTile(i, tile);
      };
 
      area.appendChild(tile);
    });
  }
 
  function revealTile(i, tile) {
     if (revealed.has(i)) return;
     revealed.add(i);
     tile.innerHTML = '';
 
    const reveal = document.createElement('div');
     reveal.className = 'sl-reveal';
 
     const img = document.createElement('img');
     img.src = cfg.config.reveal;
    reveal.appendChild(img);
    tile.appendChild(reveal);
 
    const state = states[currentTicket.outcomes[i]];
    showBubble(tile, state?.text || '');
    handleState(currentTicket.outcomes[i]);
    checkCompletion();
  }
 
  function showBubble(tile, text) {
    if (!text) return;
    const bubble = document.createElement('div');
    bubble.className = 'sl-bubble';
    bubble.textContent = text;
    tile.appendChild(bubble);
 
     const rect = tile.getBoundingClientRect();
    bubble.style.top = rect.bottom > window.innerHeight - 80 ? '-2.4em' : '100%';
    bubble.style.left = '0';
 
    setTimeout(() => bubble.remove(), 3000);
   }
   }


   function handleState(id) {
   function end() {
     const s = states[id];
     scratching = false;
    if (!s) return;
 
    if (s.type === 'emoji') mascot.textContent = s.text;
    if (id === 6) mascot.style.visibility = 'hidden';
    if (id === 10) scanBtn.classList.add('highlight');
    if (id === 11) endGame();
   }
   }


   function checkCompletion() {
   canvas.addEventListener('mousedown', start);
    if (revealed.size === 30 && currentTicket.id === 10) {
   canvas.addEventListener('mousemove', move);
      scanBtn.classList.add('highlight');
  canvas.addEventListener('mouseup', end);
    }
   canvas.addEventListener('mouseleave', end);
  }
 
   function endGame() {
    root.classList.add('sl-invert');
    setTimeout(() => {
      root.style.display = 'none';
      location.hash = '#词条正文';
    }, 1500);
   }
 
  openBtn.onclick = openTicket;
  scanBtn.onclick = () => {
    if (currentTicket && currentTicket.id === 10) openTicket();
  };


   updateInfo();
   canvas.addEventListener('touchstart', start);
  canvas.addEventListener('touchmove', move);
  canvas.addEventListener('touchend', end);
})();
})();
</script>
</script>

2026年2月1日 (日) 16:53的版本

这是一个刮刮乐 Widget


 <img class="scratch-bg"
      src="180px-%E5%9B%BE%E7%89%871.png"
      alt="lottery bg">
 <img class="scratch-reveal"
      src="180px-%E5%88%AE%E5%BC%80%E5%90%8E.jpg"
      alt="reveal">
 <canvas class="scratch-canvas"></canvas>

<style> .scratch-wrapper {

 position: relative;
 width: 360px;
 height: 360px;

}

.scratch-bg, .scratch-reveal {

 position: absolute;
 inset: 0;
 width: 100%;
 height: 100%;
 pointer-events: none;

}

.scratch-canvas {

 position: absolute;
 inset: 0;
 width: 100%;
 height: 100%;
 cursor: pointer;

} </style>

<script type="text/javascript"> (function () {

 var wrapper = document.currentScript.parentElement;
 var canvas = wrapper.querySelector('.scratch-canvas');
 var ctx = canvas.getContext('2d');
 var size = 360;
 canvas.width = size;
 canvas.height = size;
 var coverImg = new Image();
 coverImg.crossOrigin = 'anonymous';
 coverImg.src =
   'https://wm.gaoice.run/images/thumb/5/5a/%E5%88%AE%E5%BC%80%E5%89%8D.png/180px-%E5%88%AE%E5%BC%80%E5%89%8D.png';
 coverImg.onload = function () {
   ctx.drawImage(coverImg, 0, 0, size, size);
 };
 var scratching = false;
 function getPos(e) {
   var rect = canvas.getBoundingClientRect();
   var p = e.touches ? e.touches[0] : e;
   return {
     x: p.clientX - rect.left,
     y: p.clientY - rect.top
   };
 }
 function start(e) {
   scratching = true;
   e.preventDefault();
 }
 function move(e) {
   if (!scratching) return;
   e.preventDefault();
   var p = getPos(e);
   ctx.globalCompositeOperation = 'destination-out';
   ctx.beginPath();
   ctx.arc(p.x, p.y, 18, 0, Math.PI * 2);
   ctx.fill();
 }
 function end() {
   scratching = false;
 }
 canvas.addEventListener('mousedown', start);
 canvas.addEventListener('mousemove', move);
 canvas.addEventListener('mouseup', end);
 canvas.addEventListener('mouseleave', end);
 canvas.addEventListener('touchstart', start);
 canvas.addEventListener('touchmove', move);
 canvas.addEventListener('touchend', end);

})(); </script>