QuanAI只读快照

共享会话

🗺️ 島嶼點擊放大效果

分享于 2026年9月9日 17:10
提问者

當點擊一個島,無論是否選中己方棋子,這個島都將被選中並放大1.5倍。中心四個島以其自身中心放大,上、左下、右下三個島在放大時要配合translate以使其朝向中心放大並保持邊距。

ifif21.htmltext/plain · 79.4 KB预览下载
QuanAI

以下修改可讓島嶼選擇與棋子選擇互相獨立:

  • 點擊島上的任意落子點或島嶼空白區域,都會選中該島。
  • 無論是否已選中己方棋子,最後點擊的島都會放大至 1.5 倍。
  • 中間四島 1、2、3、5 原地放大。
  • 外圍三島向棋盤中心位移後放大:
    • 上島向下。
    • 左下島向右上。
    • 右下島向左上。
  • 島嶼、棋子、提示點、選擇環及點擊區域會同步變換。

1. 增加島嶼選中樣式

在 SVG 內部 <style> 中,放在 .island-copy.hit-slot 相關樣式附近:

css
.island-copy.is-selected{
  filter:url(#islandShadow) brightness(1.08);
}

.island-hit{
  opacity:0;
  pointer-events:all;
  cursor:pointer;
}

2. 增加縮放參數及選中狀態

ISLAND_CENTERS 後面加入:

js
const ISLAND_ZOOM=1.5;

/*
  外圍三島的位移量。

  位移值等於島嶼向外部分因 1.5 倍縮放增加的尺寸,
  因而能保持原本與棋盤外框的邊距:

  上島:向下
  左下島:向右上
  右下島:向左上
*/
const OUTER_ISLAND_SHIFT={
  0:[0,24.75],
  4:[24.032,-16.875],
  6:[-24.032,-16.875]
};

找到:

js
let selectedCell=-1;

改成:

js
let selectedCell=-1;
let selectedIsland=-1;

找到:

js
const islandUses=[];
const hitSlots=[];

改成:

js
const islandUses=[];
const islandHitGroups=[];
const hitSlots=[];

3. 增加島嶼變換函式

放在 cellCoordinates() 函式後面:

js
function getIslandView(island){
  if(island!==selectedIsland){
    return {
      scale:1,
      dx:0,
      dy:0
    };
  }

  const shift=OUTER_ISLAND_SHIFT[island]||[0,0];

  return {
    scale:ISLAND_ZOOM,
    dx:shift[0],
    dy:shift[1]
  };
}

/*
  先以島嶼中心為縮放中心,再套用外圍島的向內位移。
*/
function islandTransform(island){
  const [cx,cy]=ISLAND_CENTERS[island];
  const {scale,dx,dy}=getIslandView(island);

  return [
    `translate(${dx} ${dy})`,
    `translate(${cx} ${cy})`,
    `scale(${scale})`,
    `translate(${-cx} ${-cy})`
  ].join(" ");
}

/*
  取得經過島嶼縮放、位移後的畫面座標。
  用於最後一步連線這類橫跨不同島嶼的圖形。
*/
function visualCellCoordinates(index){
  const island=(index/7)|0;
  const [cx,cy]=ISLAND_CENTERS[island];
  const [x,y]=cellCoordinates(index);
  const {scale,dx,dy}=getIslandView(island);

  return [
    cx+dx+(x-cx)*scale,
    cy+dy+(y-cy)*scale
  ];
}

4. 替換 buildBoard()

把原來的整個 buildBoard() 替換成:

js
function buildBoard(){
  const islandLayer=$("#islandLayer");
  const hitLayer=$("#hitLayer");

  ISLAND_CENTERS.forEach(([x,y],island)=>{
    /*
      可見島嶼。
    */
    const use=createSVGElement("use",{
      href:"#island-symbol",
      transform:`translate(${x} ${y})`,
      class:"island-copy",
      "data-island":island
    });

    use.style.color="#fff0c5";
    islandLayer.appendChild(use);
    islandUses.push(use);

    /*
      每座島擁有獨立的點擊群組。
      群組和可見島嶼使用相同 transform,
      因而放大後點擊位置仍然正確。
    */
    const hitGroup=createSVGElement("g",{
      "data-hit-island":island
    });

    const islandHit=createSVGElement("use",{
      href:"#island-symbol",
      transform:`translate(${x} ${y})`,
      class:"island-hit",
      "data-island":island,
      "aria-hidden":"true"
    });

    hitGroup.appendChild(islandHit);

    for(let point=0;point<7;point++){
      const index=island*7+point;
      const [cellX,cellY]=cellCoordinates(index);

      const circle=createSVGElement("circle",{
        cx:cellX,
        cy:cellY,
        r:11.5,
        class:"hit-slot",
        "data-cell":index,
        "data-island":island,
        "aria-hidden":"true"
      });

      hitGroup.appendChild(circle);
      hitSlots[index]=circle;
    }

    hitLayer.appendChild(hitGroup);
    islandHitGroups.push(hitGroup);
  });
}

新增的透明 .island-hit 會覆蓋整座島,所以不必精準點在七個落子點上也能選中島嶼。


5. 修改 renderBoard()

state.claims.forEach() 中加入島嶼變換。將原來的區段:

js
state.claims.forEach((owner,index)=>{
  const use=islandUses[index];
  use.style.color=
    owner===1 ? "#ff9f82" :
    owner===-1 ? "#d7f7ff" :
    "#fff0c5";
  use.dataset.owner=String(owner);
});

替換成:

js
state.claims.forEach((owner,index)=>{
  const use=islandUses[index];
  const [x,y]=ISLAND_CENTERS[index];

  use.style.color=
    owner===1 ? "#ff9f82" :
    owner===-1 ? "#d7f7ff" :
    "#fff0c5";

  use.dataset.owner=String(owner);
  use.classList.toggle("is-selected",index===selectedIsland);

  use.setAttribute(
    "transform",
    `${islandTransform(index)} translate(${x} ${y})`
  );
});

/*
  點擊群組必須和島嶼使用相同的縮放及位移。
*/
islandHitGroups.forEach((group,index)=>{
  group.setAttribute("transform",islandTransform(index));
});

/*
  將選中島及其點擊區移到所在圖層的最後,
  避免被其他島的點擊區域遮蓋。
*/
if(selectedIsland>=0){
  $("#islandLayer").appendChild(islandUses[selectedIsland]);
  $("#hitLayer").appendChild(islandHitGroups[selectedIsland]);
}

然後刪除 renderBoard() 最後原本的整段游標判斷:

js
hitSlots.forEach((slot,index)=>{
  if(
    gameStarted &&
    !thinking &&
    !state.winner &&
    controllers[state.turn]==="human"
  ){
    slot.style.cursor=
      state.board[index]===state.turn || state.board[index]===0
        ? "pointer"
        : "not-allowed";
  }else{
    slot.style.cursor="default";
  }
});

改成:

js
hitSlots.forEach(slot=>{
  slot.style.cursor="pointer";
});

即使該位置不能落子,點擊它仍然可以選中所屬島嶼。


6. 讓棋子、提示和最後一步同步變換

修改 renderPieces()

找到:

js
const [x,y]=cellCoordinates(index);
const use=createSVGElement("use",{
  href:player===1 ? "#fire-piece" : "#ice-piece",
  transform:`translate(${x} ${y})`,
  class:"piece-use"
});

替換成:

js
const island=(index/7)|0;
const [x,y]=cellCoordinates(index);

const use=createSVGElement("use",{
  href:player===1 ? "#fire-piece" : "#ice-piece",
  transform:
    `${islandTransform(island)} translate(${x} ${y})`,
  class:"piece-use"
});

修改 renderLastMove()

找到:

js
const [x1,y1]=cellCoordinates(move.from);
const [x2,y2]=cellCoordinates(move.to);

替換成:

js
const [x1,y1]=visualCellCoordinates(move.from);
const [x2,y2]=visualCellCoordinates(move.to);

修改 renderTargets()

建立選中環的程式:

js
const [sx,sy]=cellCoordinates(selectedCell);
const ring=createSVGElement("circle",{
  cx:sx,
  cy:sy,
  r:12.5,
  class:"selection-ring",
  stroke:state.turn===1 ? "#ffdc6b" : "#75dcff"
});

替換成:

js
const selectedCellIsland=(selectedCell/7)|0;
const [sx,sy]=cellCoordinates(selectedCell);

const ring=createSVGElement("circle",{
  cx:sx,
  cy:sy,
  r:12.5,
  class:"selection-ring",
  stroke:state.turn===1 ? "#ffdc6b" : "#75dcff",
  transform:islandTransform(selectedCellIsland)
});

同一函式中,找到建立目的地提示點的程式:

js
for(const move of getMovesFrom(state,selectedCell)){
  const [x,y]=cellCoordinates(move.to);
  const marker=createSVGElement("circle",{
    cx:x,
    cy:y,
    r:4.2,
    class:"target-dot"
  });
  targetLayer.appendChild(marker);
}

替換成:

js
for(const move of getMovesFrom(state,selectedCell)){
  const destinationIsland=(move.to/7)|0;
  const [x,y]=cellCoordinates(move.to);

  const marker=createSVGElement("circle",{
    cx:x,
    cy:y,
    r:4.2,
    class:"target-dot",
    transform:islandTransform(destinationIsland)
  });

  targetLayer.appendChild(marker);
}

7. 修改點擊邏輯

將整個 onCellClick() 替換成:

js
function onIslandClick(island){
  if(!Number.isInteger(island)||island<0||island>=7) return;

  selectedIsland=island;
  renderBoard();
  renderStatus();
}

function onCellClick(index){
  /*
    島嶼選擇優先於棋子操作。

    因此即使:
    1. 尚未選中己方棋子;
    2. 點到敵方棋子;
    3. 點到不能移動的位置;
    4. AI 正在思考;
    這座島仍會被選中。
  */
  selectedIsland=(index/7)|0;

  if(
    !gameStarted ||
    thinking ||
    state.winner ||
    controllers[state.turn]!=="human"
  ){
    renderBoard();
    renderStatus();
    return;
  }

  const occupant=state.board[index];

  if(occupant===state.turn){
    lastAIStats=null;
    selectedCell=selectedCell===index ? -1 : index;
    renderBoard();
    renderStatus();
    return;
  }

  if(
    occupant===0 &&
    selectedCell>=0 &&
    isLegalMove(state,{from:selectedCell,to:index})
  ){
    /*
      selectedIsland 已經是目的地所屬島嶼,
      commitMove 後仍會保持放大。
    */
    commitMove(
      {from:selectedCell,to:index},
      "human",
      null
    );
    return;
  }

  /*
    即使不是合法移動,島嶼選擇仍然生效。
  */
  renderBoard();
  renderStatus();
}

再將 bindEvents() 中原本的:

js
$("#hitLayer").addEventListener("click",event=>{
  const target=event.target.closest(".hit-slot");
  if(target) onCellClick(Number(target.dataset.cell));
});

替換成:

js
$("#hitLayer").addEventListener("click",event=>{
  const target=event.target.closest?.(".hit-slot,.island-hit");
  if(!target) return;

  if(target.classList.contains("hit-slot")){
    onCellClick(Number(target.dataset.cell));
  }else{
    onIslandClick(Number(target.dataset.island));
  }
});

8. 重置時清除島嶼選擇

resetGame() 中,找到:

js
selectedCell=-1;

在其後加入:

js
selectedIsland=-1;

restoreHistory() 中也同樣改成:

js
selectedCell=-1;
selectedIsland=-1;

最後將 Escape 鍵相關區段:

js
}else if(selectedCell>=0){
  selectedCell=-1;
  renderBoard();
  renderStatus();
}

替換成:

js
}else if(selectedCell>=0||selectedIsland>=0){
  selectedCell=-1;
  selectedIsland=-1;
  renderBoard();
  renderStatus();
}

這樣按下 Escape 會同時取消棋子及島嶼選擇。