这是HTML文档,包含Word、Excel、PPT三种可切换背景,并支持键盘打字和像素小人交互。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>办公软件 · 像素小人</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#111;
font-family:"Microsoft YaHei",sans-serif;}
canvas{display:block;cursor:default;}
#tabs{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);
display:flex;gap:8px;z-index:10;}
#tabs button{padding:9px 18px;border:none;border-radius:20px;
background:rgba(0,0,0,.55);color:#fff;font:13px/1 "Microsoft YaHei",sans-serif;
cursor:pointer;backdrop-filter:blur(6px);transition:.2s;white-space:nowrap;}
#tabs button:hover{background:rgba(0,0,0,.78);}
#tabs button.active{background:#4ade80;color:#101010;font-weight:700;}
#tip{position:fixed;left:14px;bottom:16px;color:rgba(255,255,255,.6);
font:12px/1.7 "Microsoft YaHei",sans-serif;pointer-events:none;z-index:10;
text-shadow:0 1px 3px rgba(0,0,0,.7);}
#kb{position:fixed;left:0;top:0;width:2px;height:2px;opacity:0;
border:0;outline:0;padding:0;background:transparent;color:transparent;}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<div id="tabs">
<button data-i="0" class="active">Word 文档</button>
<button data-i="1">Excel 表格</button>
<button data-i="2">PPT 演示</button>
</div>
<div id="tip">点击小人 → 消失并补充 | 直接打字 | Tab 切换背景 | Enter 清空小人并换行</div>
<input id="kb" autocomplete="off" spellcheck="false">
<script>
(function () {
'use strict';
/* ================= 基础变量 ================= */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const kb = document.getElementById('kb');
const PIXEL = 4; // 像素点边长
const MAX_MEN = 3; // 屏幕上保持的小人数量
const LINE_H = 26; // 文字行高
// 5×8 点阵小人
const SPRITE = [
'..#..',
'.###.',
'..#..',
'#####',
'..#..',
'.#.#.',
'.#.#.',
'#...#'
];
const SW = SPRITE[0].length;
const SH = SPRITE.length;
let men = []; // 小人数组
let stopped = false; // 回车后停止补充
let themeIndex = 0; // 当前背景
let lines = ['']; // 打出的文字(每行一个字符串)
const rand = (a, b) => a + Math.random() * (b - a);
/* ================= 画布尺寸 ================= */
function resize() {
cv.width = window.innerWidth;
cv.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
/* ================= 小人逻辑 ================= */
function createMan() {
return {
x : rand(0, Math.max(10, cv.width - SW * PIXEL)),
y : rand(0, Math.max(10, cv.height - SH * PIXEL)),
vx: (Math.random() < 0.5 ? -1 : 1) * rand(0.6, 1.8),
vy: (Math.random() < 0.5 ? -1 : 1) * rand(0.6, 1.8)
};
}
function fillMen() {
if (stopped) return;
while (men.length < MAX_MEN) men.push(createMan());
}
function updateMen() {
const maxX = Math.max(0, cv.width - SW * PIXEL);
const maxY = Math.max(0, cv.height - SH * PIXEL);
for (const m of men) {
m.x += m.vx;
m.y += m.vy;
if (m.x < 0) { m.x = 0; m.vx = Math.abs(m.vx); }
if (m.x > maxX) { m.x = maxX; m.vx = -Math.abs(m.vx); }
if (m.y < 0) { m.y = 0; m.vy = Math.abs(m.vy); }
if (m.y > maxY) { m.y = maxY; m.vy = -Math.abs(m.vy); }
}
}
function drawMen() {
ctx.fillStyle = '#22c55e';
for (const m of men) {
for (let r = 0; r < SH; r++) {
for (let c = 0; c < SW; c++) {
if (SPRITE[r][c] === '#') {
ctx.fillRect(m.x + c * PIXEL, m.y + r * PIXEL, PIXEL, PIXEL);
}
}
}
}
}
/* ================= 通用:绘制标题栏 + 功能区 ================= */
function drawChrome(color, title) {
const W = cv.width;
// 标题栏
ctx.fillStyle = color;
ctx.fillRect(0, 0, W, 44);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 15px "Microsoft YaHei",sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(title, 18, 23);
// 右上角窗口按钮装饰
ctx.fillStyle = 'rgba(255,255,255,.45)';
ctx.fillRect(W - 96, 21, 12, 2);
ctx.fillRect(W - 66, 16, 12, 12);
ctx.fillStyle = 'rgba(255,255,255,.65)';
ctx.fillRect(W - 38, 21, 14, 2);
// 功能区
ctx.fillStyle = '#f7f8f9';
ctx.fillRect(0, 44, W, 46);
ctx.fillStyle = '#d9dde1';
const btnCount = Math.min(8, Math.floor((W - 40) / 50));
for (let i = 0; i < btnCount; i++) {
ctx.fillRect(18 + i * 50, 56, 36, 22);
}
ctx.fillStyle = '#e2e6e9';
ctx.fillRect(0, 90, W, 1);
}
/* ================= 背景 1:Word ================= */
function drawWord() {
const W = cv.width, H = cv.height;
ctx.fillStyle = '#e9ecef';
ctx.fillRect(0, 0, W, H);
drawChrome('#2b579a', '文档1 - Word');
// 页面
const pw = Math.min(W * 0.74, 780);
const py = 112;
const ph = Math.max(120, H - py - 26);
const px = (W - pw) / 2;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,.18)';
ctx.shadowBlur = 20;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#ffffff';
ctx.fillRect(px, py, pw, ph);
ctx.restore();
// 页面顶部一条淡色分隔,模拟页眉
ctx.fillStyle = '#eef1f4';
ctx.fillRect(px + 70, py + 34, pw - 140, 1);
return {
x: px + 70,
y: py + 58,
w: Math.max(80, pw - 140),
h: Math.max(40, ph - 110),
color: '#1f2328',
font: '16px "Microsoft YaHei",sans-serif'
};
}
/* ================= 背景 2:Excel ================= */
function colName(n) {
let s = '';
n++;
while (n > 0) {
const r = (n - 1) % 26;
s = String.fromCharCode(65 + r) + s;
n = Math.floor((n - 1) / 26);
}
return s;
}
function drawExcel() {
const W = cv.width, H = cv.height;
ctx.fillStyle = '#f1f3f4';
ctx.fillRect(0, 0, W, H);
drawChrome('#217346', '工作簿1 - Excel');
const left = 46; // 行标题宽度
const top = 118; // 表格起始 y
const colW = 92;
const rowH = LINE_H; // 与文字行高一致,视觉更整齐
const cols = Math.max(1, Math.ceil((W - left) / colW));
const rows = Math.max(1, Math.ceil((H - top) / rowH));
// 列标题
ctx.font = '12px "Microsoft YaHei",sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let c = 0; c < cols; c++) {
const x = left + c * colW;
ctx.fillStyle = '#e8ebed';
ctx.fillRect(x, top - 24, colW, 24);
ctx.fillStyle = '#5f6368';
ctx.fillText(colName(c), x + colW / 2, top - 12);
}
// 行标题
for (let r = 0; r < rows; r++) {
const y = top + r * rowH;
ctx.fillStyle = '#e8ebed';
ctx.fillRect(0, y, left, rowH);
ctx.fillStyle = '#5f6368';
ctx.fillText(String(r + 1), left / 2, y + rowH / 2);
}
// 网格线
ctx.strokeStyle = '#d5dbe1';
ctx.lineWidth = 1;
ctx.beginPath();
for (let c = 0; c <= cols; c++) {
const x = left + c * colW + 0.5;
ctx.moveTo(x, top - 24);
ctx.lineTo(x, H);
}
for (let r = 0; r <= rows; r++) {
const y = top + r * rowH + 0.5;
ctx.moveTo(0, y);
ctx.lineTo(W, y);
}
ctx.stroke();
// 当前选中单元格高亮
ctx.strokeStyle = '#217346';
ctx.lineWidth = 2;
ctx.strokeRect(left + 1, top + 1, colW - 2, rowH - 2);
return {
x: left + 10,
y: top + 5,
w: Math.max(60, W - left - 26),
h: Math.max(40, H - top - 12),
color: '#202124',
font: '15px "Microsoft YaHei",sans-serif'
};
}
/* ================= 背景 3:PPT ================= */
function drawPPT() {
const W = cv.width, H = cv.height;
ctx.fillStyle = '#2b2b30';
ctx.fillRect(0, 0, W, H);
drawChrome('#d24726', '演示文稿1 - PowerPoint');
// 幻灯片 16:9
const areaTop = 112;
const areaH = Math.max(120, H - areaTop - 26);
let sw = Math.min(W * 0.72, 900);
let sh = sw * 9 / 16;
if (sh > areaH) {
sh = areaH;
sw = sh * 16 / 9;
}
const sx = (W - sw) / 2;
const sy = areaTop + (areaH - sh) / 2;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,.55)';
ctx.shadowBlur = 26;
ctx.shadowOffsetY = 8;
ctx.fillStyle = '#ffffff';
ctx.fillRect(sx, sy, sw, sh);
ctx.restore();
// 幻灯片底部一条橙色装饰线
ctx.fillStyle = '#d24726';
ctx.fillRect(sx, sy + sh - 6, sw, 6);
return {
x: sx + sw * 0.09,
y: sy + sh * 0.14,
w: Math.max(60, sw * 0.82),
h: Math.max(40, sh * 0.7),
color: '#222222',
font: '17px "Microsoft YaHei",sans-serif'
};
}
/* ================= 文字排版与绘制 ================= */
function wrapLines(src, maxW) {
const out = [];
for (const line of src) {
if (line === '') { out.push(''); continue; }
let cur = '', w = 0;
for (const ch of line) {
const cw = ctx.measureText(ch).width;
if (w + cw > maxW && cur.length > 0) {
out.push(cur);
cur = ch;
w = cw;
} else {
cur += ch;
w += cw;
}
}
out.push(cur);
}
return out;
}
function drawText(area, t) {
if (!area) return;
ctx.save();
ctx.font = area.font;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillStyle = area.color;
const wrapped = wrapLines(lines, area.w);
const maxLines = Math.max(1, Math.floor(area.h / LINE_H));
let show = wrapped;
if (wrapped.length > maxLines) {
show = wrapped.slice(wrapped.length - maxLines);
}
for (let i = 0; i < show.length; i++) {
ctx.fillText(show[i], area.x, area.y + i * LINE_H);
}
// 光标(闪烁)
if (Math.floor(t / 530) % 2 === 0) {
const idx = show.length - 1;
const last = show[idx] || '';
const w = ctx.measureText(last).width;
ctx.fillRect(area.x + w + 1, area.y + idx * LINE_H + 3, 2, LINE_H - 8);
}
ctx.restore();
}
/* ================= 主循环 ================= */
function frame(t) {
let area;
if (themeIndex === 0) area = drawWord();
else if (themeIndex === 1) area = drawExcel();
else area = drawPPT();
updateMen();
drawMen(); // 小人画在背景之上
drawText(area, t); // 文字画在最上层,保证清晰
requestAnimationFrame(frame);
}
/* ================= 文本编辑 ================= */
function newLine() {
lines.push('');
if (lines.length > 300) lines.shift();
}
function insertText(str) {
for (const ch of str) {
if (ch === '\n') newLine();
else lines[lines.length - 1] += ch;
}
}
function backspace() {
const i = lines.length - 1;
if (lines[i].length > 0) lines[i] = lines[i].slice(0, -1);
else if (i > 0) lines.pop();
}
/* ================= 主题切换 ================= */
function setTheme(i) {
themeIndex = i;
document.querySelectorAll('#tabs button').forEach((b, idx) => {
b.classList.toggle('active', idx === i);
});
}
document.querySelectorAll('#tabs button').forEach(btn => {
btn.addEventListener('click', () => {
setTheme(Number(btn.dataset.i));
kb.focus();
});
});
/* ================= 键盘事件 ================= */
window.addEventListener('keydown', e => {
// Tab:切换背景
if (e.key === 'Tab') {
e.preventDefault();
setTheme((themeIndex + 1) % 3);
return;
}
// Enter:清空所有小人 + 文本换行
if (e.key === 'Enter') {
e.preventDefault();
if (men.length > 0) { stopped = true; men.length = 0; }
newLine();
return;
}
// Backspace:删除
if (e.key === 'Backspace') {
e.preventDefault();
backspace();
return;
}
// 其余可打印字符交给 input 事件(兼容中文输入法)
});
// 用隐藏输入框捕获输入,天然支持中文输入法
kb.addEventListener('input', e => {
if (e.isComposing) return; // 拼音组合中,忽略
const v = kb.value;
if (v) {
insertText(v);
kb.value = '';
}
});
/* ================= 鼠标点击:命中即消失 ================= */
cv.addEventListener('mousedown', e => {
const rect = cv.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
for (let i = men.length - 1; i >= 0; i--) {
const m = men[i];
const c = Math.floor((mx - m.x) / PIXEL);
const r = Math.floor((my - m.y) / PIXEL);
if (r >= 0 && r < SH && c >= 0 && c < SW && SPRITE[r][c] === '#') {
men.splice(i, 1);
fillMen();
break;
}
}
kb.focus(); // 保证键盘输入不中断
});
/* ================= 启动 ================= */
fillMen();
requestAnimationFrame(frame);
// 初始聚焦(若被浏览器拦截,点击画布后即可输入)
setTimeout(() => kb.focus(), 100);
})();
</script>
</body>
</html>
```
玩法与输入体验
三种办公背景一键切换,打字和点小人互不干扰。
· 背景切换:底部按钮或按 Tab 键在 Word、Excel、PPT 间循环切换,每种背景都有对应的界面细节。
· 打字输入:点击画面任意处激活隐藏输入框,直接敲键盘即可在背景的“文档区”看到文字,支持中文输入法、退格和换行。
· 小人交互:点击任意像素小人,它会消失并随机补充一个新小人,总数始终维持 3 个;按 Enter 键清空所有小人并换行。
· 文字排版:文字会按当前编辑区域的宽度自动换行,光标闪烁提示输入位置。
---
优化建议: 可以调整代码开头的 MAX_MEN 变量改变小人总数,修改 rand(0.6, 1.8) 中的数值控制移动速度,或调整 PIXEL 改变像素点大小。
仅供参考用,实际使用还需实验。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95242675/article/details/167699323




