敲地鼠 · 相册皮肤
by CrystalMeteor49693 lines26.0 KB
!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>敲地鼠 · 相册皮肤</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:system-ui,"PingFang SC","Microsoft YaHei",sans-serif}
body{background:#F5EBD6;color:#1B2E28;padding:10px;display:flex;justify-content:center}
.app{width:100%;max-width:470px;display:flex;flex-direction:column;gap:8px}
.hud{display:flex;gap:6px}
.stat{flex:1;background:#FFFDF6;border:2.5px solid #1B2E28;border-radius:12px;box-shadow:3px 3px 0 #1B2E28;padding:5px 8px}
.stat b{display:block;font-size:10px;letter-spacing:.16em;color:#4A6156;font-weight:500}
.stat span{font-size:20px;font-weight:900;font-variant-numeric:tabular-nums}
.board{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;padding:9px;aspect-ratio:1/1;
background:#BFD4A7;border:3px solid #1B2E28;border-radius:16px;box-shadow:4px 4px 0 #1B2E28;overflow:hidden;touch-action:none}
.hole{position:relative;border:0;background:transparent;padding:0;overflow:hidden;border-radius:10px;cursor:pointer;touch-action:none}
.hole::before{content:"";position:absolute;left:8%;right:8%;bottom:4%;height:42%;border-radius:50%;
background:#25382D;border:2.5px solid #1B2E28;box-shadow:inset 0 5px 0 rgba(255,255,255,.1),inset 0 -7px 9px rgba(0,0,0,.4)}
.mole{position:absolute;left:6%;bottom:1%;width:88%;aspect-ratio:1/1;object-fit:contain;
transform:translateY(115%) scale(.9);opacity:0;transition:transform .16s cubic-bezier(.2,1.3,.5,1),opacity .12s;z-index:2}
.mole.up{transform:translateY(0) scale(1);opacity:1}
.mole.fx{transform:translateY(6%) scale(1.12,.82);transition:transform .09s}
.dock{display:flex;gap:6px;flex-wrap:wrap}
.btn{height:44px;padding:0 12px;border:2.5px solid #1B2E28;border-radius:12px;background:#FFFDF6;
box-shadow:3px 3px 0 #1B2E28;font-size:13px;font-weight:700;color:#1B2E28;cursor:pointer}
.btn:active{transform:translate(2px,2px);box-shadow:1px 1px 0 #1B2E28}
.btn.pk{background:#D94F3D;color:#fff}
.btn.yl{background:#F2B33F}
.tip{font-size:11px;color:#4A6156;line-height:1.6}
.overlay{position:fixed;inset:0;background:rgba(27,46,40,.55);display:none;align-items:center;justify-content:center;padding:12px;z-index:50}
.overlay.on{display:flex}
.panel{width:min(430px,96vw);max-height:calc(100dvh - 24px);overflow-y:auto;background:#F5EBD6;
border:3px solid #1B2E28;border-radius:18px;box-shadow:6px 6px 0 #1B2E28;padding:14px;display:flex;flex-direction:column;gap:10px}
.head{display:flex;align-items:center;justify-content:space-between}
.head h2{font-size:19px}
.x{width:36px;height:36px;border:2.5px solid #1B2E28;border-radius:10px;background:#fff;box-shadow:2px 2px 0 #1B2E28;font-size:18px;font-weight:700;cursor:pointer}
.pad{position:relative;width:100%;aspect-ratio:1/1;border:3px solid #1B2E28;border-radius:14px;overflow:hidden;background:#fff}
.pad canvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;display:block}
.row{display:flex;align-items:center;gap:8px;background:#FFFDF6;border:2.5px solid #1B2E28;border-radius:12px;padding:7px 10px}
.row label{font-size:12px;font-weight:700;color:#4A6156;white-space:nowrap}
input[type=range]{flex:1;accent-color:#1B2E28;height:22px}
.sw{display:flex;gap:6px;flex-wrap:wrap}
.sw button{width:32px;height:32px;border:2.5px solid #1B2E28;border-radius:9px;box-shadow:2px 2px 0 #1B2E28;cursor:pointer}
.sw button.on{outline:2.5px dashed #1B2E28;outline-offset:2px}
.sw button.er{width:auto;padding:0 10px;background:#FFFDF6;font-size:12px;font-weight:700}
.sw button.er.on{background:#1B2E28;color:#fff}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
.card{position:relative;display:flex;flex-direction:column;align-items:center;gap:5px;padding:7px 5px;border:2.5px solid #1B2E28;
border-radius:13px;background:#FFFDF6;box-shadow:3px 3px 0 #1B2E28;cursor:pointer}
.card.on{background:#FFF4CE}
.thumb{width:100%;aspect-ratio:1/1;border-radius:50%;background:#F7E7D2 center/cover no-repeat;border:3px solid #fff;box-shadow:0 0 0 2px #1B2E28}
.card span{font-size:11px;font-weight:700;line-height:1.3;word-break:break-all;text-align:center}
.tag{position:absolute;top:-8px;right:-6px;font-size:9px;padding:1px 5px;border:2px solid #1B2E28;border-radius:6px;background:#D94F3D;color:#fff;transform:rotate(6deg)}
.del{position:absolute;top:-8px;left:-6px;width:20px;height:20px;border:2px solid #1B2E28;border-radius:50%;background:#EEDDBD;font-size:11px;font-weight:700;cursor:pointer}
.name{min-height:42px;padding:0 10px;border:2.5px solid #1B2E28;border-radius:11px;background:#fff;font-size:13px}
.msg{font-size:12px;font-weight:700;color:#D94F3D;min-height:15px}
.empty{grid-column:1/-1;padding:16px;border:2.5px dashed rgba(27,46,40,.4);border-radius:12px;text-align:center;font-size:12.5px;color:#4A6156;line-height:1.7}
.toast{position:fixed;left:50%;bottom:90px;transform:translate(-50%,10px);z-index:80;padding:9px 16px;border:2.5px solid #1B2E28;
border-radius:999px;background:#1B2E28;color:#fff;font-size:12.5px;font-weight:700;opacity:0;pointer-events:none;transition:.2s;text-align:center}
.toast.on{opacity:1;transform:translate(-50%,0)}
</style>
</head>
<body>
<div class="app">
<div class="hud">
<div class="stat"><b>分数</b><span id="score">0</span></div>
<div class="stat"><b>最高</b><span id="best">0</span></div>
<div class="stat"><b>秒数</b><span id="time">60</span></div>
</div>
<div class="board" id="board"></div>
<div class="dock">
<button class="btn pk" id="startBtn" type="button">开始敲</button>
<button class="btn" id="albumBtn" type="button">相册</button>
<button class="btn" id="drawBtn" type="button">画皮肤</button>
<button class="btn" id="bagBtn" type="button">背包</button>
<button class="btn" id="soundBtn" type="button">声音 开</button>
</div>
<p class="tip">点「开始敲」开始。地鼠冒出来就点它;自己画的、相册里的照片都能当地鼠皮肤。</p>
</div>
<div class="toast" id="toast"></div>
<!-- 画皮肤 -->
<div class="overlay" id="drawWrap">
<div class="panel">
<div class="head"><h2>画一只地鼠</h2><button class="x" data-close="drawWrap" type="button">×</button></div>
<div class="pad"><canvas id="guideCv" width="600" height="600"></canvas><canvas id="drawCv" width="600" height="600"></canvas></div>
<div class="sw" id="swWrap"></div>
<div class="row"><label>画笔</label><input type="range" id="brush" min="2" max="48" value="14"></div>
<div class="row" style="justify-content:space-between">
<button class="btn" id="undoBtn" type="button">撤销</button>
<button class="btn" id="clearBtn" type="button">清空</button>
</div>
<input class="name" id="drawName" type="text" maxlength="8" placeholder="给它起个名字">
<p class="msg" id="drawMsg"></p>
<button class="btn pk" id="saveDraw" type="button">保存到背包</button>
</div>
</div>
<!-- 相册皮肤 -->
<div class="overlay" id="photoWrap">
<div class="panel">
<div class="head"><h2>相册皮肤</h2><button class="x" data-close="photoWrap" type="button">×</button></div>
<div class="pad"><canvas id="photoCv" width="600" height="600"></canvas></div>
<div class="row"><label>放大</label><input type="range" id="zoom" min="1" max="3" step="0.02" value="1"></div>
<div class="row" style="justify-content:space-between">
<button class="btn" id="reloadBtn" type="button">换一张照片</button>
</div>
<input class="name" id="photoName" type="text" maxlength="8" placeholder="给它起个名字">
<p class="msg" id="photoMsg">用手指拖动照片,把脸放进圆里</p>
<button class="btn pk" id="savePhoto" type="button">保存到背包</button>
</div>
</div>
<!-- 背包 -->
<div class="overlay" id="bagWrap">
<div class="panel">
<div class="head"><h2>背包</h2><button class="x" data-close="bagWrap" type="button">×</button></div>
<div class="grid3" id="bagGrid"></div>
<button class="btn yl" id="toDraw" type="button">再画一个</button>
</div>
</div>
<input type="file" id="photoInput" accept="image/*" hidden>
<script>
(function(){
"use strict";
/* ---------- 存档 ---------- */
var KEY_SKIN="wk.skins.v2", KEY_BEST="wk.best.v2", KEY_EQ="wk.equip.v2", MAX_USER=8;
var best=0;
try{ best=parseInt(localStorage.getItem(KEY_BEST),10)||0; }catch(e){}
document.getElementById("best").textContent=best;
/* ---------- 内置皮肤(用代码画,不依赖外部图片) ---------- */
function builtinSkin(fur, nose, ear){
var c=document.createElement("canvas"); c.width=c.height=512;
var g=c.getContext("2d");
g.fillStyle=fur;
g.beginPath(); g.ellipse(140,150,62,58,0,0,7); g.ellipse(372,150,62,58,0,0,7); g.fill();
g.beginPath(); g.arc(256,268,200,0,7); g.fill();
g.fillStyle="rgba(0,0,0,.10)";
g.beginPath(); g.arc(256,300,200,0.4,2.74); g.fill();
g.fillStyle="#fff";
g.beginPath(); g.arc(180,240,46,0,7); g.arc(332,240,46,0,7); g.fill();
g.fillStyle="#1B2E28";
g.beginPath(); g.arc(186,246,24,0,7); g.arc(326,246,24,0,7); g.fill();
g.fillStyle="#fff";
g.beginPath(); g.arc(194,236,8,0,7); g.arc(334,236,8,0,7); g.fill();
g.fillStyle=nose;
g.beginPath(); g.ellipse(256,300,34,26,0,0,7); g.fill();
g.fillStyle="#fff";
g.fillRect(236,342,40,46);
g.fillStyle="rgba(27,46,40,.35)";
g.fillRect(252,342,7,46);
g.fillStyle="#1B2E28";
g.beginPath(); g.moveTo(256,344); g.quadraticCurveTo(214,330,200,300);
g.moveTo(256,344); g.quadraticCurveTo(298,330,312,300); g.lineWidth=6; g.strokeStyle="#1B2E28"; g.stroke();
return c.toDataURL("image/png");
}
var BUILTIN=[
{id:"b1",name:"棕地鼠",src:builtinSkin("#A9764A","#E88AA6","#8C5A32")},
{id:"b2",name:"金地鼠",src:builtinSkin("#F2C24D","#E88AA6","#C99A2E")},
{id:"b3",name:"薄荷地鼠",src:builtinSkin("#A9DCCB","#E88AA6","#7FBBA6")}
];
var userSkins=[];
try{
var raw=localStorage.getItem(KEY_SKIN);
if(raw) userSkins=JSON.parse(raw)||[];
}catch(e){}
var equipped="b1";
try{
var eq=localStorage.getItem(KEY_EQ);
if(eq) equipped=eq;
}catch(e){}
function save(){
try{ localStorage.setItem(KEY_SKIN,JSON.stringify(userSkins)); }catch(e){}
}
(function checkEquip(){
var l=BUILTIN.concat(userSkins), ok=false;
for(var i=0;i<l.length;i++){ if(l[i].id===equipped) ok=true; }
if(!ok) equipped="b1";
})();
function allSkins(){ return BUILTIN.concat(userSkins); }
function skinById(id){
var l=allSkins();
for(var i=0;i<l.length;i++){ if(l[i].id===id) return l[i]; }
return BUILTIN[0];
}
function skinSrc(id){ return skinById(id).src||skinById(id).data; }
/* ---------- 音效 ---------- */
var actx=null, muted=false;
function audio(){
if(!actx){
var AC=window.AudioContext||window.webkitAudioContext;
if(!AC) return null;
actx=new AC();
}
if(actx.state==="suspended") actx.resume();
return actx;
}
function thump(gold){
if(muted) return;
var c=audio(); if(!c) return;
var t=c.currentTime+0.001;
var o=c.createOscillator(), g=c.createGain();
o.type="triangle";
var f0=gold?250:196;
o.frequency.setValueAtTime(f0,t);
o.frequency.exponentialRampToValueAtTime(f0*0.42,t+0.17);
g.gain.setValueAtTime(0.0001,t);
g.gain.exponentialRampToValueAtTime(0.55,t+0.01);
g.gain.exponentialRampToValueAtTime(0.001,t+0.24);
o.connect(g); g.connect(c.destination);
o.start(t); o.stop(t+0.27);
var len=Math.floor(c.sampleRate*0.1);
var buf=c.createBuffer(1,len,c.sampleRate);
var d=buf.getChannelData(0);
for(var i=0;i<len;i++){ var k=1-i/len; d[i]=(Math.random()*2-1)*k*k; }
var s=c.createBufferSource(); s.buffer=buf;
var lp=c.createBiquadFilter(); lp.type="lowpass"; lp.frequency.value=850;
var ng=c.createGain(); ng.gain.value=0.2;
s.connect(lp); lp.connect(ng); ng.connect(c.destination);
s.start(t);
}
function puff(){
if(muted) return;
var c=audio(); if(!c) return;
var t=c.currentTime+0.001;
var o=c.createOscillator(), g=c.createGain();
o.type="sine";
o.frequency.setValueAtTime(120,t);
o.frequency.exponentialRampToValueAtTime(75,t+0.12);
g.gain.setValueAtTime(0.14,t);
g.gain.exponentialRampToValueAtTime(0.001,t+0.13);
o.connect(g); g.connect(c.destination);
o.start(t); o.stop(t+0.15);
}
document.getElementById("soundBtn").onclick=function(){
muted=!muted;
this.textContent=muted?"声音 关":"声音 开";
};
/* ---------- 场地 ---------- */
var board=document.getElementById("board");
var holes=[];
for(var i=0;i<9;i++){
(function(idx){
var h=document.createElement("button");
h.className="hole"; h.type="button";
h.innerHTML='<img class="mole" alt="地鼠">';
var img=h.querySelector(".mole");
h.addEventListener("pointerdown",function(ev){
ev.preventDefault();
tap(idx);
});
board.appendChild(h);
holes.push({el:h,img:img,on:false,timer:0});
})(i);
}
/* ---------- 游戏 ---------- */
var G={run:false,score:0,left:60,active:0,tick:0,spawner:0,endTimer:0};
var scoreEl=document.getElementById("score");
var timeEl=document.getElementById("time");
var startBtn=document.getElementById("startBtn");
function resetHoles(){
holes.forEach(function(h){
clearTimeout(h.timer);
h.on=false; h.img.classList.remove("up","fx");
});
G.active=0;
}
function start(){
if(G.run) return;
resetHoles();
G.run=true; G.score=0; G.left=60; G.active=0;
scoreEl.textContent="0";
timeEl.textContent="60";
startBtn.textContent="敲着呢";
G.endTimer=setInterval(function(){
G.left--;
timeEl.textContent=G.left;
if(G.left<=0) stop();
},1000);
schedule();
}
function stop(){
G.run=false;
clearInterval(G.endTimer);
clearTimeout(G.spawner);
resetHoles();
startBtn.textContent="再来一局";
if(G.score>best){
best=G.score;
document.getElementById("best").textContent=best;
try{ localStorage.setItem(KEY_BEST,String(best)); }catch(e){}
}
toast("这局 "+G.score+" 分,最高 "+best+" 分");
}
function schedule(){
if(!G.run) return;
var fast=Math.max(300,760-(60-G.left)*6);
G.spawner=setTimeout(function(){
spawn();
schedule();
},fast);
}
function spawn(){
if(!G.run) return;
if(G.active>=2) return;
var free=holes.filter(function(h){ return !h.on; });
if(!free.length) return;
var h=free[Math.floor(Math.random()*free.length)];
h.on=true;
h.img.src=skinSrc(equipped);
h.img.classList.remove("fx");
h.img.classList.add("up");
G.active++;
var stay=Math.max(430,1080-(60-G.left)*7);
h.timer=setTimeout(function(){
if(h.on){
h.on=false; G.active=Math.max(0,G.active-1);
h.img.classList.remove("up");
}
},stay);
}
function tap(idx){
var h=holes[idx];
if(!G.run) return;
if(!h.on){
puff();
return;
}
clearTimeout(h.timer);
h.on=false;
G.active=Math.max(0,G.active-1);
G.score++;
scoreEl.textContent=G.score;
thump(isGold(h.img.src));
h.img.classList.add("fx");
setTimeout(function(){ h.img.classList.remove("up","fx"); },110);
}
function isGold(src){ return src===BUILTIN[1].src; }
/* ---------- 提示条 ---------- */
var toastEl=document.getElementById("toast"), toastT=0;
function toast(msg){
toastEl.textContent=msg;
toastEl.classList.add("on");
clearTimeout(toastT);
toastT=setTimeout(function(){ toastEl.classList.remove("on"); },2200);
}
/* ---------- 通用遮罩 ---------- */
function openWrap(id){
var els=document.querySelectorAll(".overlay");
for(var i=0;i<els.length;i++) els[i].classList.remove("on");
document.getElementById(id).classList.add("on");
if(G.run){ clearInterval(G.endTimer); clearTimeout(G.spawner); G.paused=true; }
}
function closeWraps(){
var els=document.querySelectorAll(".overlay");
for(var i=0;i<els.length;i++) els[i].classList.remove("on");
if(G.run&&G.paused){ G.paused=false; G.endTimer=setInterval(function(){
G.left--; timeEl.textContent=G.left; if(G.left<=0) stop(); },1000); schedule(); }
}
document.querySelectorAll("[data-close]").forEach(function(b){
b.addEventListener("click",function(){ closeWraps(); });
});
/* ---------- 画皮肤 ---------- */
var guideCv = document.getElementById("guideCv");
var gc = guideCv.getContext("2d");
var drawCv = document.getElementById("drawCv");
var dc = drawCv.getContext("2d");
var brush = document.getElementById("brush");
var COLORS = ["#1B2E28","#D94F3D","#F2B33F","#4C8A5E","#2E6B86","#8C5FA8","#E88AA6","#FFFFFF"];
var pen = { color:COLORS[0], size:14, erase:false, down:false, last:null };
var undo = [];
function paintGuide(){
gc.clearRect(0,0,600,600);
gc.strokeStyle = "rgba(27,46,40,.22)";
gc.lineWidth = 4;
gc.setLineDash([14,12]);
gc.beginPath();
gc.ellipse(120,130,66,60,0,0,7);
gc.ellipse(480,130,66,60,0,0,7);
gc.stroke();
gc.beginPath();
gc.arc(300,318,232,0,7);
gc.stroke();
gc.setLineDash([]);
gc.fillStyle = "rgba(27,46,40,.30)";
gc.font = '600 26px system-ui, "PingFang SC", sans-serif';
gc.textAlign = "center";
gc.fillText("在这里画你的地鼠", 300, 110);
}
paintGuide();
(function buildSw(){
var wrap = document.getElementById("swWrap");
COLORS.forEach(function(c){
var b = document.createElement("button");
b.type = "button";
b.style.background = c;
b.setAttribute("data-color", c);
b.addEventListener("click", function(){ pen.color = c; pen.erase = false; refreshSw(); });
wrap.appendChild(b);
});
var er = document.createElement("button");
er.type = "button";
er.className = "er";
er.textContent = "橡皮";
er.addEventListener("click", function(){ pen.erase = !pen.erase; refreshSw(); });
wrap.appendChild(er);
refreshSw();
})();
function refreshSw(){
Array.prototype.forEach.call(document.getElementById("swWrap").children, function(b){
if(b.classList.contains("er")){
b.classList.toggle("on", pen.erase);
}else{
b.classList.toggle("on", !pen.erase && b.getAttribute("data-color") === pen.color);
}
});
}
function clearArt(){
dc.clearRect(0,0,600,600);
undo = [];
}
function pos(e){
var r = drawCv.getBoundingClientRect();
return { x:(e.clientX-r.left)/r.width*600, y:(e.clientY-r.top)/r.height*600 };
}
drawCv.addEventListener("pointerdown", function(e){
e.preventDefault();
drawCv.setPointerCapture(e.pointerId);
try{ undo.push(dc.getImageData(0,0,600,600)); if(undo.length > 6) undo.shift(); }catch(err){}
pen.down = true;
pen.last = pos(e);
stroke(pen.last, { x:pen.last.x+0.1, y:pen.last.y+0.1 });
});
drawCv.addEventListener("pointermove", function(e){
if(!pen.down) return;
e.preventDefault();
stroke(pen.last, pos(e));
});
function endPen(){ pen.down = false; }
drawCv.addEventListener("pointerup", endPen);
drawCv.addEventListener("pointercancel", endPen);
drawCv.addEventListener("pointerleave", endPen);
function stroke(a,b){
dc.globalCompositeOperation = pen.erase ? "destination-out" : "source-over";
dc.strokeStyle = pen.color;
dc.lineWidth = pen.size;
dc.lineCap = "round";
dc.lineJoin = "round";
dc.beginPath();
dc.moveTo(a.x,a.y);
dc.lineTo(b.x,b.y);
dc.stroke();
pen.last = b;
}
brush.addEventListener("input", function(){ pen.size = Number(this.value) || 14; });
document.getElementById("undoBtn").onclick = function(){
var s = undo.pop();
if(!s) return;
dc.globalCompositeOperation = "source-over";
dc.putImageData(s,0,0);
};
document.getElementById("clearBtn").onclick = function(){
try{ undo.push(dc.getImageData(0,0,600,600)); }catch(err){}
dc.globalCompositeOperation = "source-over";
dc.clearRect(0,0,600,600);
};
document.getElementById("drawBtn").onclick = function(){
clearArt();
paintGuide();
document.getElementById("drawName").value = "";
document.getElementById("drawMsg").textContent = "";
openWrap("drawWrap");
};
function exportDraw(){
var out = document.createElement("canvas");
out.width = out.height = 512;
var og = out.getContext("2d");
var k = 512/600;
og.fillStyle = "#fff";
og.beginPath();
og.ellipse(120*k,130*k,66*k,60*k,0,0,7);
og.ellipse(480*k,130*k,66*k,60*k,0,0,7);
og.arc(300*k,318*k,232*k,0,7);
og.fill();
og.drawImage(drawCv, 0, 0, 512, 512);
return out.toDataURL("image/png");
}
document.getElementById("saveDraw").onclick = function(){
var msg = document.getElementById("drawMsg");
if(userSkins.length >= MAX_USER){ msg.textContent = "背包放满了,先删一个"; return; }
var name = document.getElementById("drawName").value.trim() || ("我的皮肤 " + (userSkins.length+1));
userSkins.unshift({ id:"u"+Date.now(), name:name.slice(0,8), data:exportDraw() });
save();
renderBag();
closeWraps();
toast("画好啦,去背包穿上它");
};
/* ---------- 相册皮肤 ---------- */
var photoCv=document.getElementById("photoCv"), pc=photoCv.getContext("2d");
var photo={img:null,scale:1,ox:0,oy:0,down:false,lx:0,ly:0};
var zoom=document.getElementById("zoom");
var photoInput=document.getElementById("photoInput");
var CX=300,CY=318,R=232;
function renderPhoto(guide){
pc.save();
pc.setTransform(1,0,0,1,0,0);
pc.fillStyle="#fff"; pc.fillRect(0,0,600,600);
pc.beginPath(); pc.ellipse(120,130,66,60,0,0,7); pc.ellipse(480,130,66,60,0,0,7);
pc.arc(CX,CY,R,0,7); pc.fill();
if(photo.img){
var minS=Math.max((R*2)/photo.img.width,(R*2)/photo.img.height);
var s=minS*photo.scale;
var w=photo.img.width*s, h=photo.img.height*s;
pc.save();
pc.beginPath(); pc.arc(CX,CY,R,0,7); pc.clip();
pc.drawImage(photo.img, CX+photo.ox-w/2, CY+photo.oy-h/2, w, h);
pc.restore();
}
if(guide){
pc.strokeStyle="rgba(27,46,40,.25)"; pc.lineWidth=4; pc.setLineDash([14,12]);
pc.beginPath(); pc.arc(CX,CY,R,0,7); pc.stroke(); pc.setLineDash([]);
}
pc.restore();
}
renderPhoto(true);
function clampPhoto(){
if(!photo.img) return;
var minS=Math.max((R*2)/photo.img.width,(R*2)/photo.img.height);
var s=minS*photo.scale;
var w=photo.img.width*s, h=photo.img.height*s;
var mx=Math.max(0,w/2-R), my=Math.max(0,h/2-R);
photo.ox=Math.max(-mx,Math.min(mx,photo.ox));
photo.oy=Math.max(-my,Math.min(my,photo.oy));
}
photoCv.addEventListener("pointerdown",function(e){
if(!photo.img) return;
e.preventDefault();
photoCv.setPointerCapture(e.pointerId);
photo.down=true; photo.lx=e.clientX; photo.ly=e.clientY;
});
photoCv.addEventListener("pointermove",function(e){
if(!photo.down) return;
e.preventDefault();
var r=photoCv.getBoundingClientRect();
photo.ox+=(e.clientX-photo.lx)/r.width*600;
photo.oy+=(e.clientY-photo.ly)/r.height*600;
photo.lx=e.clientX; photo.ly=e.clientY;
clampPhoto(); renderPhoto(true);
});
function endPhoto(){ photo.down=false; }
photoCv.addEventListener("pointerup",endPhoto);
photoCv.addEventListener("pointercancel",endPhoto);
photoCv.addEventListener("pointerleave",endPhoto);
zoom.addEventListener("input",function(){
photo.scale=Number(this.value)||1; clampPhoto(); renderPhoto(true);
});
document.getElementById("albumBtn").onclick=function(){ photoInput.click(); };
document.getElementById("reloadBtn").onclick=function(){ photoInput.click(); };
photoInput.addEventListener("change",function(){
var f=photoInput.files&&photoInput.files[0];
if(!f) return;
var fr=new FileReader();
fr.onload=function(){
var img=new Image();
img.onload=function(){
photo.img=img; photo.scale=1; photo.ox=0; photo.oy=0;
zoom.value="1";
document.getElementById("photoName").value="";
document.getElementById("photoMsg").textContent="用手指拖动照片,把脸放进圆里";
renderPhoto(true);
openWrap("photoWrap");
};
img.onerror=function(){ toast("这张图打不开,换一张"); };
img.src=fr.result;
};
fr.readAsDataURL(f);
photoInput.value="";
});
document.getElementById("savePhoto").onclick=function(){
var msg=document.getElementById("photoMsg");
if(!photo.img){ msg.textContent="先选一张照片"; return; }
if(userSkins.length>=MAX_USER){ msg.textContent="背包放满了,先删一个"; return; }
var out=document.createElement("canvas"); out.width=out.height=512;
var oc=out.getContext("2d");
oc.fillStyle="#fff"; oc.fillRect(0,0,512,512);
var k=512/600;
oc.save();
oc.beginPath(); oc.arc(CX*k,CY*k,R*k,0,7); oc.clip();
var minS=Math.max((R*2)/photo.img.width,(R*2)/photo.img.height);
var s=minS*photo.scale*k;
var w=photo.img.width*s, h=photo.img.height*s;
oc.drawImage(photo.img,(CX+photo.ox)*k-w/2,(CY+photo.oy)*k-h/2,w,h);
oc.restore();
var name=document.getElementById("photoName").value.trim()||("相册皮肤 "+(userSkins.length+1));
userSkins.unshift({id:"p"+Date.now(),name:name.slice(0,8),data:out.toDataURL("image/jpeg",0.82)});
save(); renderBag(); closeWraps();
toast("照片皮肤好啦,去背包穿上它");
};
/* ---------- 背包 ---------- */
var bagGrid=document.getElementById("bagGrid");
var pending=null;
function renderBag(){
bagGrid.innerHTML="";
allSkins().forEach(function(sk){
var card=document.createElement("div");
card.className="card"+(sk.id===equipped?" on":"");
card.innerHTML='<div class="thumb"></div><span></span>';
card.querySelector(".thumb").style.backgroundImage='url("'+(sk.src||sk.data)+'")';
card.querySelector("span").textContent=sk.name;
if(sk.id===equipped){
var t=document.createElement("div"); t.className="tag"; t.textContent="穿着";
card.appendChild(t);
}
if(sk.id.indexOf("u")===0||sk.id.indexOf("p")===0){
var d=document.createElement("button");
d.className="del"; d.type="button"; d.textContent="×";
d.addEventListener("click",function(ev){
ev.stopPropagation();
if(pending===sk.id){
userSkins=userSkins.filter(function(x){ return x.id!==sk.id; });
if(equipped===sk.id) equipped="b1";
save(); renderBag(); toast("删掉了");
pending=null;
}else{
pending=sk.id; toast("再点一次 × 就删掉");
}
});
card.appendChild(d);
}
card.addEventListener("click",function(){
equipped=sk.id;
try{ localStorage.setItem(KEY_EQ,equipped); }catch(e){}
renderBag();
closeWraps();
toast("穿上了 "+sk.name);
});
bagGrid.appendChild(card);
});
}
document.getElementById("bagBtn").onclick=function(){ renderBag(); openWrap("bagWrap"); };
document.getElementById("toDraw").onclick=function(){
resetDraw();
document.getElementById("drawName").value="";
document.getElementById("drawMsg").textContent="";
openWrap("drawWrap");
};
/* ---------- 开始 ---------- */
startBtn.addEventListener("click",function(){
audio();
start();
});
renderBag();
})();
</script>
</body>
</html>Game Source: 敲地鼠 · 相册皮肤
Creator: CrystalMeteor49
Libraries: none
Complexity: complex (693 lines, 26.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-crystalmeteor49-mw70qu00" to link back to the original. Then publish at arcadelab.ai/publish.