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

/*
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();
}
}