|
|
| 第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> |
这是一个刮刮乐 Widget
<img class="scratch-bg"
src="
"
alt="lottery bg">
<img class="scratch-reveal"
src="
"
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>