You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
414 lines
11 KiB
414 lines
11 KiB
/*
|
|
author: leeenx
|
|
@ 贪吃蛇的 view 类
|
|
*/
|
|
// 链表类
|
|
import Chain from "../lib/utils/Chain";
|
|
import food from "../food.png";
|
|
|
|
const startColor = 0xfec321;
|
|
const endColor = 0xe29b01;
|
|
const rotate = (cx, cy, point, rad) => {
|
|
const { x, y } = point;
|
|
let cos = Math.cos(rad),
|
|
sin = Math.sin(rad),
|
|
nx = cos * (x - cx) + sin * (y - cy) + cx,
|
|
ny = cos * (y - cy) - sin * (x - cx) + cy;
|
|
point.x = nx;
|
|
point.y = ny;
|
|
};
|
|
|
|
const getDir = (x1, y1, x2, y2) => {
|
|
return x2 > x1 ? "right" : x2 < x1 ? "left" : y2 > y1 ? "bottom" : "top";
|
|
};
|
|
// view 类
|
|
export default class view {
|
|
// 构造函数
|
|
constructor(dom, width, height) {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.style.width = width + "px";
|
|
canvas.style.height = height + "px";
|
|
canvas.width = width * window.devicePixelRatio;
|
|
canvas.height = height * window.devicePixelRatio;
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
dom.appendChild(canvas);
|
|
// 创建 view 的蛇
|
|
this.snake = new Chain();
|
|
|
|
// 保证 updateTicker 指针永远指向 view
|
|
this.updateTicker = this.updateTicker.bind(this);
|
|
|
|
// 挂载到this
|
|
this.canvas = canvas;
|
|
this.ctx = ctx;
|
|
this.foodImg = new Image(120, 120);
|
|
this.foodImg.src = food;
|
|
}
|
|
|
|
// 初始化
|
|
init(config = {}) {
|
|
// 蛇的尺寸挂载到 config
|
|
config.size = {
|
|
width: config.width / config.column,
|
|
height: config.height / config.row,
|
|
};
|
|
// 初化data
|
|
this.data = config.data;
|
|
|
|
// 全局 config 挂载
|
|
this.config = config;
|
|
|
|
// 食物
|
|
this.food = {};
|
|
this.food.visible = false;
|
|
|
|
// 通过 model.zone 创建一张快速定位表
|
|
this.createQuickMap(this.data.zone);
|
|
|
|
// 同步 model 的初始数据
|
|
for (let { data } of this.data.snake) {
|
|
this.snake.push(data);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.snake.clean();
|
|
}
|
|
|
|
// 快速寻位表
|
|
createQuickMap(map) {
|
|
let { width, height } = this.config.size;
|
|
// 快速表
|
|
this.quickMap = [];
|
|
for (let { col, row } of map) {
|
|
this.quickMap.push([col * width, row * height]);
|
|
}
|
|
}
|
|
|
|
// 快速计算position
|
|
getPosition(index) {
|
|
return this.quickMap[index];
|
|
}
|
|
|
|
// 随机生成食物
|
|
feed(index) {
|
|
this.food.visible = 1;
|
|
this.food.dirty++;
|
|
this.food.clearDirty++;
|
|
const [left, top] = this.getPosition(index);
|
|
Object.assign(this.food, { left, top });
|
|
}
|
|
|
|
// ticker update
|
|
updateTicker() {
|
|
// this.render();
|
|
}
|
|
|
|
// 状态更新
|
|
update(data) {
|
|
// 食物更新
|
|
this.food !== data.food && this.feed(data.food);
|
|
this.updateDelta(data.snake);
|
|
}
|
|
|
|
// 增量更新
|
|
updateDelta(snakeA, snakeB = this.snake) {
|
|
// snakeA === model.snake, snakeB === view.snake
|
|
this.updateTail(snakeA, snakeB)
|
|
.then(() => this.updateHead(snakeA, snakeB))
|
|
.catch(() => this.wholeUpdate(snakeA, snakeB))
|
|
.then(() => this.render());
|
|
}
|
|
|
|
// 检查蛇头
|
|
updateHead(snakeA, snakeB) {
|
|
return new Promise((resolve, reject) => {
|
|
// snakeA 与 snakeB 做头指针比较
|
|
let headA,
|
|
headB = snakeB.first();
|
|
// 指针指向头部
|
|
snakeA.setPointer(snakeA.HEAD);
|
|
while (snakeB.length <= snakeA.length) {
|
|
headA = snakeA.next();
|
|
// 头节点匹配
|
|
if (headA.data === headB.data) {
|
|
// 执行 then 通道
|
|
return resolve();
|
|
}
|
|
// 不匹配
|
|
else {
|
|
// 向snakeB插入头节点
|
|
if (snakeA.HEAD === headA.index) {
|
|
snakeB.unshift(headA.data);
|
|
}
|
|
// 向snakeB插入第二个节点
|
|
else {
|
|
snakeB.insertAfter(0, headA.data);
|
|
}
|
|
}
|
|
}
|
|
// 头指针未匹配上,走 catch 通道
|
|
reject();
|
|
});
|
|
}
|
|
|
|
// 检查蛇尾
|
|
updateTail(snakeA, snakeB) {
|
|
return new Promise((resolve, reject) => {
|
|
// snakeA 与 snakeB 做尾指针比较
|
|
let tailA = snakeA.last(),
|
|
tailB;
|
|
while (snakeB.length !== 0) {
|
|
tailB = snakeB.last();
|
|
// 尾节点匹配
|
|
if (tailA.data === tailB.data) {
|
|
// 执行 then 通道
|
|
return resolve();
|
|
}
|
|
// 不匹配
|
|
else {
|
|
snakeB.pop();
|
|
}
|
|
}
|
|
// 尾指针未匹配上,走 catch 通道
|
|
reject();
|
|
});
|
|
}
|
|
|
|
// 全量更新
|
|
wholeUpdate(snakeA, snakeB) {
|
|
console.log(">>>>>>>>>>>>>>>>>", "low performance");
|
|
// 把视图上的蛇回收
|
|
while (snakeB.length !== 0) {
|
|
snakeB.pop();
|
|
}
|
|
// 重头开始插入 snake
|
|
for (let { data } of snakeA) {
|
|
snakeB.unshift(data);
|
|
}
|
|
}
|
|
|
|
renderFood() {
|
|
let { width, height } = this.config.size;
|
|
if (this.food && this.food.visible) {
|
|
this.ctx.drawImage(
|
|
this.foodImg,
|
|
this.food.left,
|
|
this.food.top,
|
|
width,
|
|
height
|
|
);
|
|
}
|
|
}
|
|
drawHeadTail(x, y, dir, index, length) {
|
|
const { ctx } = this;
|
|
let { width, height } = this.config.size;
|
|
const sWidth = (width / 120) * 90;
|
|
const r = sWidth / 2;
|
|
const gap = (width - sWidth) / 2;
|
|
const a = { x, y: y + height };
|
|
const b = { x: x + gap, y: a.y };
|
|
const c = { x: x + width / 2, y: a.y };
|
|
const d = { x: x + gap + sWidth, y: a.y };
|
|
const e = { x: x + width, y: a.y };
|
|
const f = { x: b.x, y: y + r };
|
|
const g = { x: b.x, y };
|
|
const h = { x: c.x, y };
|
|
const i = { x: d.x, y };
|
|
const j = { x: d.x, y: f.y };
|
|
const k = { x: x + width / 2, y: y - index * height };
|
|
const l = { x: k.x, y: y + (length - index) * height };
|
|
let rad =
|
|
dir === "bottom"
|
|
? 0
|
|
: dir === "top"
|
|
? Math.PI
|
|
: dir === "left"
|
|
? -Math.PI / 2
|
|
: Math.PI / 2;
|
|
if (index) rad += Math.PI;
|
|
[a, b, c, d, e, f, g, h, i, j, k, l].forEach((point) =>
|
|
rotate(x + width / 2, y + height / 2, point, rad)
|
|
);
|
|
|
|
// k
|
|
// |
|
|
// |
|
|
// |
|
|
// g---h---i
|
|
// | / \ |
|
|
// f j
|
|
// | |
|
|
// a-b---c---d-e
|
|
// |
|
|
// |
|
|
// |
|
|
// l
|
|
|
|
const gradient = ctx.createLinearGradient(k.x, k.y, l.x, l.y);
|
|
gradient.addColorStop(0, "#" + startColor.toString(16));
|
|
gradient.addColorStop(1, "#" + endColor.toString(16));
|
|
ctx.fillStyle = gradient;
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(b.x, b.y);
|
|
ctx.lineTo(f.x, f.y);
|
|
ctx.arcTo(g.x, g.y, h.x, h.y, r);
|
|
ctx.arcTo(i.x, i.y, j.x, j.y, r);
|
|
ctx.lineTo(d.x, d.y);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
drawRec(x, y, dir, i, length) {
|
|
const { ctx } = this;
|
|
let { width, height } = this.config.size;
|
|
const sWidth = (width / 120) * 90;
|
|
const gap = (width - sWidth) / 2;
|
|
const a = { x: x + gap, y: y + height };
|
|
const b = { x: a.x, y };
|
|
const c = { x: x + gap + sWidth, y };
|
|
const d = { x: c.x, y: a.y };
|
|
const e = { x: x + width / 2, y: y - i * height };
|
|
const f = { x: e.x, y: y + (length - i) * height };
|
|
let rad =
|
|
dir === "bottom"
|
|
? 0
|
|
: dir === "top"
|
|
? Math.PI
|
|
: dir === "left"
|
|
? -Math.PI / 2
|
|
: Math.PI / 2;
|
|
[a, b, c, d, e, f].forEach((point) =>
|
|
rotate(x + width / 2, y + height / 2, point, rad)
|
|
);
|
|
|
|
// e
|
|
// |
|
|
// b---c
|
|
// | |
|
|
// a---d
|
|
// |
|
|
// f
|
|
|
|
const gradient = ctx.createLinearGradient(e.x, e.y, f.x, f.y);
|
|
gradient.addColorStop(0, "#" + startColor.toString(16));
|
|
gradient.addColorStop(1, "#" + endColor.toString(16));
|
|
ctx.fillStyle = gradient;
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.lineTo(c.x, c.y);
|
|
ctx.lineTo(d.x, d.y);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
drawCorner(x, y, dir, i, length) {
|
|
const { ctx } = this;
|
|
let { width, height } = this.config.size;
|
|
const sWidth = (width / 120) * 90;
|
|
const gap = (width - sWidth) / 2;
|
|
const sqrt2 = width / 2 / Math.sqrt(2);
|
|
const a = { x: x + gap, y: y + height };
|
|
const b = { x: x + gap + sWidth, y: a.y };
|
|
const c = { x: b.x, y: y + gap + sWidth };
|
|
const d = { x: x + width, y: c.y };
|
|
const e = { x: d.x, y: y + gap };
|
|
const f = { x: a.x, y: e.y };
|
|
let g = {
|
|
x: x + width / 2 + ((length - i) * 2 + 1) * sqrt2,
|
|
y: y + height / 2 + ((length - i) * 2 + 1) * sqrt2,
|
|
};
|
|
let h = {
|
|
x: x + width / 2 - (i * 2 + 1) * sqrt2,
|
|
y: y + height / 2 - (i * 2 + 1) * sqrt2,
|
|
};
|
|
// if (["rightbottom", "lefttop", "topright", "bottomleft"].includes(dir)) {
|
|
// let tmp = h;
|
|
// h = g;
|
|
// g = tmp;
|
|
// }
|
|
let rad =
|
|
dir === "bottomright" || dir === "rightbottom"
|
|
? 0
|
|
: dir === "topleft" || dir === "lefttop"
|
|
? Math.PI
|
|
: dir === "righttop" || dir === "topright"
|
|
? Math.PI / 2
|
|
: -Math.PI / 2;
|
|
[a, b, c, d, e, f, g, h].forEach((point) =>
|
|
rotate(x + width / 2, y + height / 2, point, rad)
|
|
);
|
|
|
|
//
|
|
// g
|
|
// /
|
|
// -------------
|
|
// | |
|
|
// ----f-------e
|
|
// | | |
|
|
// | | c---d
|
|
// | | | |
|
|
// ----a---b----
|
|
// /
|
|
// h
|
|
//
|
|
|
|
const gradient = ctx.createLinearGradient(h.x, h.y, g.x, g.y);
|
|
gradient.addColorStop(0, "#" + startColor.toString(16));
|
|
gradient.addColorStop(1, "#" + endColor.toString(16));
|
|
ctx.fillStyle = gradient;
|
|
ctx.strokeStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(a.x, a.y);
|
|
ctx.lineTo(b.x, b.y);
|
|
ctx.arcTo(c.x, c.y, d.x, d.y, gap);
|
|
ctx.lineTo(e.x, e.y);
|
|
ctx.arcTo(f.x, f.y, a.x, a.y, gap + sWidth);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
renderSnake() {
|
|
const { length } = this.snake;
|
|
const list = [];
|
|
for (let body of this.snake) {
|
|
list.push(body);
|
|
}
|
|
list.forEach(({ data }, i) => {
|
|
const pre = list[i - 1];
|
|
const nxt = list[i + 1];
|
|
const [x, y] = this.getPosition(data);
|
|
if (i === 0) {
|
|
// head
|
|
const [xNxt, yNxt] = this.getPosition(nxt.data);
|
|
const dir = getDir(x, y, xNxt, yNxt);
|
|
this.drawHeadTail(x, y, dir, i, list.length);
|
|
} else if (i === length - 1) {
|
|
const [xPre, yPre] = this.getPosition(pre.data);
|
|
const dir = getDir(xPre, yPre, x, y);
|
|
this.drawHeadTail(x, y, dir, i, list.length);
|
|
} else {
|
|
const [xNxt, yNxt] = this.getPosition(nxt.data);
|
|
const dir1 = getDir(x, y, xNxt, yNxt);
|
|
const [xPre, yPre] = this.getPosition(pre.data);
|
|
const dir2 = getDir(xPre, yPre, x, y);
|
|
if (dir1 === dir2) {
|
|
this.drawRec(x, y, dir1, i, length);
|
|
} else {
|
|
const dir3 = getDir(x, y, xPre, yPre);
|
|
this.drawCorner(x, y, dir1 + dir3, i, length);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// 渲染
|
|
render() {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.renderFood();
|
|
this.renderSnake();
|
|
}
|
|
}
|
|
|