
让平面棋子立体起来——Canvas径向渐变的实战应用设计截图代码如下/** * ChessBoardView.ets - 可复用的五子棋棋盘组件 * 使用Canvas绘制棋盘、网格、棋子 */import{BOARD_SIZE,EMPTY,BLACK,WHITE}from./GameConstants;constBOARD_BG_COLOR:string#D4A868;constLINE_COLOR:string#5C4033;constSTAR_COLOR:string#3B2F2F;constLAST_MOVE_COLOR:string#E63946;Componentexportstruct ChessBoardView{/** 棋盘数据扁平数组row*15col */PropWatch(onDataChange)boardData:number[][];/** 最后一手行 */ProplastMoveRow:number-1;/** 最后一手列 */ProplastMoveCol:number-1;/** 点击回调 */onCellClick:(row:number,col:number)void(){};privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(newRenderingContextSettings(true));privatecanvasSize:number320;privateisReady:booleanfalse;build(){Canvas(this.context).width(100%).aspectRatio(1).onReady((){this.isReadytrue;this.drawBoard();}).onClick((event:ClickEvent){this.handleClick(event);}).onAreaChange((_oldValue:Area,newValue:Area){constwnewValue.width;if(typeofwnumber){this.canvasSizew;if(this.isReady){this.drawBoard();}}})}/** 数据变化时重绘 */onDataChange():void{if(this.isReady){this.drawBoard();}}/** 处理点击事件 */privatehandleClick(event:ClickEvent):void{constcellSizethis.canvasSize/BOARD_SIZE;constxevent.x;constyevent.y;constcolMath.round((x-cellSize/2)/cellSize);constrowMath.round((y-cellSize/2)/cellSize);if(row0rowBOARD_SIZEcol0colBOARD_SIZE){this.onCellClick(row,col);}}/** 绘制棋盘 */privatedrawBoard():void{constctxthis.context;constsizethis.canvasSize;if(size0)return;constcellSizesize/BOARD_SIZE;consthalfCellcellSize/2;// 清空画布ctx.clearRect(0,0,size,size);// 绘制背景ctx.fillStyleBOARD_BG_COLOR;ctx.fillRect(0,0,size,size);// 绘制网格线ctx.strokeStyleLINE_COLOR;ctx.lineWidth1;for(leti0;iBOARD_SIZE;i){constposhalfCelli*cellSize;// 水平线ctx.beginPath();ctx.moveTo(halfCell,pos);ctx.lineTo(size-halfCell,pos);ctx.stroke();// 垂直线ctx.beginPath();ctx.moveTo(pos,halfCell);ctx.lineTo(pos,size-halfCell);ctx.stroke();}// 绘制星位conststars:number[][][[3,3],[3,11],[7,7],[11,3],[11,11]];ctx.fillStyleSTAR_COLOR;for(conststarofstars){constsxhalfCellstar[1]*cellSize;constsyhalfCellstar[0]*cellSize;ctx.beginPath();ctx.arc(sx,sy,3,0,Math.PI*2);ctx.fill();}// 绘制棋子for(leti0;iBOARD_SIZE;i){for(letj0;jBOARD_SIZE;j){constidxi*BOARD_SIZEj;if(idxthis.boardData.length){constpiecethis.boardData[idx];if(piece!EMPTY){constpxhalfCellj*cellSize;constpyhalfCelli*cellSize;constradiuscellSize*0.42;this.drawPiece(ctx,px,py,radius,piece);}}}}// 绘制最后一手标记if(this.lastMoveRow0this.lastMoveCol0){constpxhalfCellthis.lastMoveCol*cellSize;constpyhalfCellthis.lastMoveRow*cellSize;ctx.strokeStyleLAST_MOVE_COLOR;ctx.lineWidth2;ctx.beginPath();ctx.arc(px,py,cellSize*0.16,0,Math.PI*2);ctx.stroke();}}/** 绘制单个棋子带渐变效果 */privatedrawPiece(ctx:CanvasRenderingContext2D,x:number,y:number,radius:number,player:number):void{if(playerBLACK){constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,#666666);gradient.addColorStop(0.5,#2A2A2A);gradient.addColorStop(1,#111111);ctx.fillStylegradient;}else{constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,#FFFFFF);gradient.addColorStop(0.7,#F0F0F0);gradient.addColorStop(1,#C8C8C8);ctx.fillStylegradient;}ctx.beginPath();ctx.arc(x,y,radius,0,Math.PI*2);ctx.fill();// 棋子边框ctx.strokeStyleplayerBLACK?#000000:#999999;ctx.lineWidth0.5;ctx.stroke();}}drawPiece方法privatedrawPiece(ctx:CanvasRenderingContext2D,x:number,y:number,radius:number,player:number):void{if(playerBLACK){constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,// 内圆偏左上的亮点x,y,radius// 外圆棋子边缘);gradient.addColorStop(0,#666666);// 中心浅灰高光gradient.addColorStop(0.5,#2A2A2A);// 中间深灰gradient.addColorStop(1,#111111);// 边缘近黑ctx.fillStylegradient;}else{constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,#FFFFFF);// 中心纯白高光gradient.addColorStop(0.7,#F0F0F0);// 中间浅灰gradient.addColorStop(1,#C8C8C8);// 边缘灰ctx.fillStylegradient;}ctx.beginPath();ctx.arc(x,y,radius,0,Math.PI*2);ctx.fill();// 棋子边框ctx.strokeStyleplayerBLACK?#000000:#999999;ctx.lineWidth0.5;ctx.stroke();}createRadialGradient参数解析ctx.createRadialGradient(x0,y0,r0,x1,y1,r1)参数含义本项目的值x0, y0内圆中心高光位置x - radius0.3, y - radius0.3r0内圆半径radius * 0.1x1, y1外圆中心棋子中心x, yr1外圆半径radius高光位置偏移内圆中心偏左上(x - r*0.3, y - r*0.3)模拟从左上方照射的光源。这是经典的球体光照模型。渐变效果图解黑棋渐变 白棋渐变 ◐ ← 高光(#666) ◑ ← 高光(#FFF) ◑◑ ◐◑ ◑◉◑ ← 中间(#2A2A) ◑◉◑ ← 中间(#F0F0) ◑◑ ◐◑ ◑ ← 边缘(#111) ◐ ← 边缘(#C8C8)addColorStop颜色停靠点// 黑棋gradient.addColorStop(0,#666666);// 0%高光区gradient.addColorStop(0.5,#2A2A2A);// 50%过渡区gradient.addColorStop(1,#111111);// 100%边缘区// 白棋gradient.addColorStop(0,#FFFFFF);// 0%高光区gradient.addColorStop(0.7,#F0F0F0);// 70%过渡区gradient.addColorStop(1,#C8C8C8);// 100%边缘区黑棋vs白棋的渐变差异黑棋高光在50%处就过渡到深色——因为黑色物体反射少白棋高光持续到70%——因为白色物体反射多这种细微差异让黑白棋子有不同质感的视觉表现。棋子边框ctx.strokeStyleplayerBLACK?#000000:#999999;ctx.lineWidth0.5;ctx.stroke();黑棋边框纯黑#000000白棋边框浅灰#999999线宽0.5像素——细线不会喧宾夺主边框的作用是让棋子在棋盘背景上更加分明特别是白棋在浅色背景上容易消失。棋子半径计算constradiuscellSize*0.42;棋子半径是格子尺寸的42%——留出16%的间隙让棋子之间不会紧贴。cellSize ├────────┤ ◯ ◯ ← 棋子直径 cellSize * 0.84间隙 cellSize * 0.16最后一手标记if(this.lastMoveRow0this.lastMoveCol0){constpxhalfCellthis.lastMoveCol*cellSize;constpyhalfCellthis.lastMoveRow*cellSize;ctx.strokeStyleLAST_MOVE_COLOR;// 红色 #E63946ctx.lineWidth2;ctx.beginPath();ctx.arc(px,py,cellSize*0.16,0,Math.PI*2);ctx.stroke();}设计思路红色小圆圈标记最后一手半径cellSize * 0.16——比棋子小不会遮挡stroke()而非fill()——空心圈不遮挡棋子这个标记对用户体验很重要——玩家一眼就能看到对方刚下的位置。总结棋子绘制展示了Canvas 2D的进阶技巧createRadialGradient径向渐变实现球体光照效果高光偏移内圆偏左上模拟光源颜色停靠点不同位置设置不同颜色实现质感细边框增加棋子与背景的对比度最后一手标记红色空心圈提供视觉反馈这些技巧让简单的黑白棋子拥有了立体感和质感大大提升了游戏的视觉品质。