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.
 
 

438 lines
12 KiB

/*
author: leeenx
@ 贪吃蛇的 view 类
*/
// 链表类
import Chain from "../lib/utils/Chain";
import food from "../food.png";
import TWEEN from "@tweenjs/tween.js";
import eye from "../eye.png";
import mouth from "../mouth.png";
import tear from "../tear.png";
import stars from "../stars.png";
const startColor = "#fec321";
const endColor = "#e29b01";
// 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;
this.eyeImg = new Image(143, 65);
this.eyeImg.src = eye;
this.mouthImg = new Image(131, 67);
this.mouthImg.src = mouth;
this.tearImg = new Image(129, 154);
this.tearImg.src = tear;
this.starsImg = new Image(224, 162);
this.starsImg.src = stars;
}
// 初始化
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;
this.lastFood = null;
// 通过 model.zone 创建一张快速定位表
this.createQuickMap(this.data.zone);
this.tail = null;
// 同步 model 的初始数据
for (let { data } of this.data.snake) {
this.snake.push(data);
}
}
setInterval(interval, timer) {
this.interval = interval;
this.timer = timer;
}
destroy() {
this.destroyed = true;
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, { index, left, top });
}
// ticker update
updateTicker() {
// this.render();
}
// 状态更新
update(data) {
// 食物更新
if (this.food.index !== data.food) {
this.lastFood = this.food.index;
this.feed(data.food);
}
this.tail = this.snake.last();
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 {
this.tail = 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(t) {
let { width, height } = this.config.size;
const ratio = Math.abs(t - 0.5) * 0.5 + 1;
if (this.food && this.food.visible) {
this.ctx.drawImage(
this.foodImg,
this.food.left - (width * (ratio - 1)) / 2,
this.food.top - (height * (ratio - 1)) / 2,
width * ratio,
height * ratio
);
}
}
renderSnake(t, lose) {
let { length } = this.snake;
let { width, height } = this.config.size;
if (lose) t = 1 - lose;
const list = [];
for (let body of this.snake) {
list.push(body);
}
let lastFoodPos =
this.lastFood === null ? this.lastFood : this.getPosition(this.lastFood);
lastFoodPos = lastFoodPos
? { x: lastFoodPos[0] + width / 2, y: lastFoodPos[1] + height / 2 }
: lastFoodPos;
const points = list.map(({ data }) => {
const [x, y] = this.getPosition(data);
return { x: x + width / 2, y: y + height / 2 };
});
const { ctx } = this;
const lineWidth = (width / 120) * 90;
let pre = null;
let headPos = null;
let headRad = null;
if (this.tail !== null) {
let [x, y] = this.getPosition(this.tail.data);
x = x + width / 2;
y = y + height / 2;
points.push({ x, y });
length++;
}
points.forEach(({ x, y }, i) => {
if (!pre) {
pre = { x, y };
} else {
const start =
i === 1 ? { x: (pre.x - x) * t + x, y: (pre.y - y) * t + y } : pre;
const end =
i === length - 1
? {
x: (x - pre.x) * (1 - t) + pre.x,
y: (y - pre.y) * (1 - t) + pre.y,
}
: { x, y };
const isV = pre.x === x;
const gStart = {
x: isV ? x : x - i * (x - pre.x),
y: isV ? y - i * (y - pre.y) : y,
};
const gEnd = {
x: isV ? x : x + (length - i) * (x - pre.x),
y: isV ? y + (length - i) * (y - pre.y) : y,
};
const gradient = ctx.createLinearGradient(
gStart.x,
gStart.y,
gEnd.x,
gEnd.y
);
gradient.addColorStop(0, startColor);
gradient.addColorStop(1, endColor);
ctx.strokeStyle = gradient;
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineCap = "round";
ctx.lineWidth = lineWidth;
ctx.lineTo(end.x, end.y);
ctx.stroke();
ctx.closePath();
if (
lastFoodPos &&
Math.abs(lastFoodPos.x - x) < 1 &&
Math.abs(lastFoodPos.y - y) < 1
) {
if (i === length - 2) this.lastFood = null;
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(
x,
y,
(lineWidth + (width - lineWidth) * (1 - i / length)) / 2,
0,
Math.PI * 2
);
ctx.fill();
}
if (i === 1) {
headPos = start;
headRad = Math.atan2(end.y - pre.y, end.x - pre.x) - Math.PI / 2;
}
pre = { x, y };
}
});
lose
? this.renderTear(headPos, headRad, lose)
: this.renderEye(headPos, headRad);
const foodPos = {
x: this.food.left + width / 2,
y: this.food.top + height / 2,
};
const foodInRange =
Math.abs(foodPos.x - headPos.x) <= width * 2 &&
Math.abs(foodPos.y - headPos.y) <= width * 2;
if (foodInRange && !lose) this.renderMouth(headPos, headRad, t);
}
renderMouth(pos, rad, t) {
t = 1;
let { width, height } = this.config.size;
const r = (width / 120) * 90;
const { ctx } = this;
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(rad);
const imgHeight = (width / this.mouthImg.width) * this.mouthImg.height;
ctx.drawImage(
this.mouthImg,
-width / 2,
-height / 2 + imgHeight / 4,
width,
imgHeight * t
);
ctx.restore();
}
renderTear(pos, rad, t) {
let { width } = this.config.size;
const r = ((width / 120) * 90) / 2;
const { ctx } = this;
{
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(rad);
const displayWidth = (width / 120) * this.tearImg.width;
const displayHeight = (width / 120) * this.tearImg.height;
ctx.drawImage(
this.tearImg,
-displayWidth / 2,
-displayHeight / 2 + (width / 120) * 100 - r,
displayWidth,
displayHeight
);
ctx.restore();
}
{
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(rad);
const displayWidth = (width / 120) * this.starsImg.width * t;
const displayHeight = (width / 120) * this.starsImg.height * t;
ctx.drawImage(
this.starsImg,
-displayWidth / 2,
-displayHeight / 2 + (width / 120) * 100 - r,
displayWidth,
displayHeight
);
ctx.restore();
}
}
renderEye(pos, rad) {
let { width } = this.config.size;
const r = ((width / 120) * 90) / 2;
const { ctx } = this;
ctx.save();
ctx.translate(pos.x, pos.y);
ctx.rotate(rad);
const displayWidth = (width / 120) * this.eyeImg.width;
const displayHeight = (width / 120) * this.eyeImg.height;
ctx.drawImage(
this.eyeImg,
-displayWidth / 2,
-displayHeight / 2 + (width / 120) * 100 - r,
displayWidth,
displayHeight
);
ctx.restore();
}
renderLose() {
return new Promise((resolve) => {
this.currentTween && this.currentTween.stop();
const tween = new TWEEN.Tween({ t: 0 });
tween.easing(TWEEN.Easing.Elastic.Out);
tween.to({ t: 1 }, 500);
tween.onComplete(resolve);
tween.onUpdate(({ t }) => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.renderFood(1);
this.renderSnake(1, t);
requestAnimationFrame(() => {
TWEEN.update();
});
});
requestAnimationFrame(() => {
TWEEN.update();
});
tween.start();
});
}
// 渲染
render() {
if (this.destroyed) return;
if (this.interval) {
const tween = new TWEEN.Tween({ t: 0 });
this.currentTween = tween;
tween.to({ t: 1 }, this.interval);
tween.onUpdate(({ t }) => {
if (this.destroyed) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.renderFood(t);
this.renderSnake(t);
requestAnimationFrame(() => {
TWEEN.update();
});
});
requestAnimationFrame(() => {
TWEEN.update();
});
tween.start();
} else {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.renderFood(0);
this.renderSnake(0);
}
}
}