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