QuanAI只读快照

共享会话

增加功能

分享于 2026年9月8日 23:59
提问者

增加功能 當在導出中選擇「數字」時,程式應自動調整預覽區中的clone中的g#etanidrop的translate和scale,以使所有棋子都居中顯示在視圖中並留有邊距,就跟「動畫」最後把鏡頭拉到使所有棋子可見一樣。找到跟「動畫」一樣的程式邏輯,套用到「數字」中。

在「數字」導出的svg中,增加一個「記分標誌」,跟「動畫」的最上面最中間的一樣,大小也是48*48,位置默認在右下角,保持一定的邊距(跟棋子與邊的距離一樣)。

在「數字」的右邊增加一個svg圖標按鈕(包含在label內),這個按鈕的圖案是「四個小三角形位於左上、右上、左下、右下」,當點擊時,它將高亮,再次點擊則恢復,當它高亮時,預覽區的「左上、右上、左下、右下」四個地方中的三個(排除記分標誌所在)位置,都將出現一個透明度為0.777的記分標誌,絕對定位在預覽區svg元素的上方,並沒有加進預覽區真實的clone的svg內部,當點擊其中一個半透明的記分標誌,則clone的svg內部真實的記分標誌就會移到那個位置,同時所有的半透明標誌消失,按鈕也不再高亮。這個位置會被記住,下次再導出「數字」則會把標誌顯示在同一位置。

回答要求:請明確給出所要修改的地方和所要替換的代碼。

trigolden_game2668.htmltext/plain · 353.8 KB预览下载
QuanAI

以下修改均針對附件 trigolden_game2668.html。核心做法是:

  • 將「動畫」使用的 getDownloadFullView() 全景計算抽成可指定可視範圍的共用邏輯。
  • 「數字」模式把 clone 中的 g#etanidrop 改成全景 translate + scale
  • 在「數字」SVG 根節點加入真正的 48×48 記分標誌。
  • 在預覽容器上方另外建立三個 HTML 絕對定位按鈕;它們不會進入下載 SVG。
  • 使用 localStorage 保存記分標誌角落。

一、修改下載預覽區 CSS

找到原本這兩段:

css
#download-preview {
  flex: none;
  display: block;
  overflow: hidden;
  background: white;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
}

#download-preview object {
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  overflow: hidden;
  background: #fff;
}

替換成:

css
#download-preview {
  position: relative;
  isolation: isolate;
  flex: none;
  display: block;
  overflow: hidden;
  background: white;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
}

#download-preview object {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  overflow: hidden;
  background: #fff;
}

/* 「數字」選項內的記分標誌位置按鈕 */
.download-option-number {
  padding-right: 7px;
  gap: 8px;
}

.download-marker-position-btn {
  width: 28px;
  height: 28px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.06);
  color: #9ca4ad;
  cursor: pointer;
  transition:
    color 0.2s,
    background 0.2s,
    box-shadow 0.2s,
    transform 0.12s;
}

.download-marker-position-btn:hover {
  color: #fff;
  background: rgba(114, 137, 218, 0.28);
}

.download-marker-position-btn:active {
  transform: scale(0.9);
}

.download-marker-position-btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.download-marker-position-btn.active {
  color: #fff;
  background: #7289da;
  box-shadow: 0 0 11px rgba(114, 137, 218, 0.9);
}

.download-marker-position-btn svg {
  display: block;
  width: 20px;
  height: 20px;
  fill: currentColor;
  pointer-events: none;
}

/*
 * 預覽區上方的三個半透明記分標誌。
 * 它們是 HTML 按鈕,不屬於預覽 object 內的真實 SVG。
 */
.download-marker-choice {
  appearance: none;
  position: absolute;
  z-index: 3;
  width: 10%;
  height: 10%;
  margin: 0;
  padding: 0;
  border: none;
  outline: none;
  background: transparent;
  opacity: 0.777;
  cursor: pointer;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.35));
  transition:
    filter 0.16s,
    transform 0.16s;
}

.download-marker-choice:hover,
.download-marker-choice:focus-visible {
  filter:
    drop-shadow(0 0 3px rgba(255, 255, 255, 0.95))
    drop-shadow(0 0 7px rgba(114, 137, 218, 0.95));
  transform: scale(1.08);
}

.download-marker-choice[data-corner='tl'] {
  top: 2.5%;
  left: 2.5%;
}

.download-marker-choice[data-corner='tr'] {
  top: 2.5%;
  right: 2.5%;
}

.download-marker-choice[data-corner='bl'] {
  bottom: 2.5%;
  left: 2.5%;
}

.download-marker-choice[data-corner='br'] {
  right: 2.5%;
  bottom: 2.5%;
}

.download-marker-choice svg {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

其中:

  • 48 / 480 = 10%,所以候選標誌尺寸為預覽區的 10%
  • 12 / 480 = 2.5%,所以四邊距離為 2.5%
  • 這和後面棋子全景使用的 12 單位邊距一致。

二、替換「數字」選項的 HTML

找到:

html
<label class="download-option">
  <input type="checkbox" id="download-option-number" onchange="handleDownloadOptionChange('number')" />
  <span class="download-checkmark" aria-hidden="true">
    <svg viewBox="0 0 24 24">
      <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
    </svg>
  </span>
  <span>數字</span>
</label>

替換成:

html
<label class="download-option download-option-number">
  <input type="checkbox" id="download-option-number" onchange="handleDownloadOptionChange('number')" />

  <span class="download-checkmark" aria-hidden="true">
    <svg viewBox="0 0 24 24">
      <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
    </svg>
  </span>

  <span>數字</span>

  <span
    id="download-marker-position-btn"
    class="download-marker-position-btn"
    role="button"
    tabindex="0"
    aria-label="選擇記分標誌位置"
    aria-pressed="false"
    onclick="toggleDownloadMarkerPicker(event)"
    onkeydown="handleDownloadMarkerPickerKeydown(event)"
  >
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <!-- 左上 -->
      <path d="M3 3h6L3 9z" />

      <!-- 右上 -->
      <path d="M15 3h6v6z" />

      <!-- 左下 -->
      <path d="M3 15l6 6H3z" />

      <!-- 右下 -->
      <path d="M21 15v6h-6z" />
    </svg>
  </span>
</label>

這裡用帶有 role="button"span,避免在 label 中嵌套原生 <button> 所產生的標籤點擊衝突。


三、增加記分標誌狀態與保存位置

找到:

js
let downloadSvgSource = '';
let downloadSvgBlob = null;
let downloadPreviewUrl = '';

替換成:

js
let downloadSvgSource = '';
let downloadSvgBlob = null;
let downloadPreviewUrl = '';

const DOWNLOAD_NUMBER_MARGIN = 12;
const DOWNLOAD_NUMBER_MARKER_SIZE = 48;
const DOWNLOAD_NUMBER_MARKER_STORAGE_KEY = 'bt-download-number-marker-corner';

const DOWNLOAD_NUMBER_MARKER_CORNERS = new Set(['tl', 'tr', 'bl', 'br']);

function loadDownloadNumberMarkerCorner() {
  try {
    const savedCorner = localStorage.getItem(DOWNLOAD_NUMBER_MARKER_STORAGE_KEY);

    if (DOWNLOAD_NUMBER_MARKER_CORNERS.has(savedCorner)) {
      return savedCorner;
    }
  } catch (error) {
    /* file:// 或禁止 localStorage 時,退回本次頁面內保存。 */
  }

  return 'br';
}

function saveDownloadNumberMarkerCorner() {
  try {
    localStorage.setItem(DOWNLOAD_NUMBER_MARKER_STORAGE_KEY, downloadNumberMarkerCorner);
  } catch (error) {
    /* localStorage 不可用時,downloadNumberMarkerCorner 仍會在本頁中保存。 */
  }
}

/* 默認右下角。 */
let downloadNumberMarkerCorner = loadDownloadNumberMarkerCorner();

/* 控制四角候選標誌是否顯示。 */
let downloadMarkerPickerOpen = false;

/* 避免同一頁中的漸層 ID 重複。 */
let downloadMarkerGraphicSerial = 0;

這樣不但關閉再打開下載窗口後仍會記住位置,重新載入頁面後也會繼續保留。


四、加入記分標誌建立函數和角落選擇功能

在下面這個函數結束後:

js
function createDownloadSvgElement(tagName, attributes = {}) {
  const element = document.createElementNS(DOWNLOAD_SVG_NS, tagName);

  Object.entries(attributes).forEach(([name, value]) => {
    if (value !== undefined && value !== null) {
      element.setAttribute(name, String(value));
    }
  });

  return element;
}

緊接著加入:

js
/*
 * 建立和動畫狀態欄中相同圖案的記分標誌。
 * 外層是帶 viewBox 的獨立 SVG,因此 width/height 可明確設為 48×48。
 */
function createDownloadScoringMarkerSvg(attributes = {}) {
  const markerSvg = createDownloadSvgElement('svg', {
    viewBox: '0 0 120 120',
    preserveAspectRatio: 'xMidYMid meet',
    width: DOWNLOAD_NUMBER_MARKER_SIZE,
    height: DOWNLOAD_NUMBER_MARKER_SIZE,
    ...attributes
  });

  const defs = createDownloadSvgElement('defs');
  const gradientId = `bt-download-score-gradient-${++downloadMarkerGraphicSerial}`;

  if (SCORING_MODE === 0) {
    const gradient = createDownloadSvgElement('linearGradient', {
      id: gradientId,
      x1: '0',
      y1: '0',
      x2: '0',
      y2: '1'
    });

    gradient.appendChild(
      createDownloadSvgElement('stop', {
        offset: '0%',
        'stop-color': '#7289da'
      })
    );

    gradient.appendChild(
      createDownloadSvgElement('stop', {
        offset: '100%',
        'stop-color': '#7289da',
        'stop-opacity': '0'
      })
    );

    defs.appendChild(gradient);
    markerSvg.appendChild(defs);

    markerSvg.appendChild(
      createDownloadSvgElement('path', {
        d: 'M4,4 L14.397,36 H105.602 L116,4',
        fill: 'none',
        stroke: '#7289da',
        'stroke-width': '4'
      })
    );

    markerSvg.appendChild(
      createDownloadSvgElement('polygon', {
        points: '32.707,120 60,36 87.293,120',
        fill: `url(#${gradientId})`,
        stroke: 'none'
      })
    );
  } else {
    const gradient = createDownloadSvgElement('linearGradient', {
      id: gradientId,
      x1: '0',
      y1: '0',
      x2: '1',
      y2: '0'
    });

    gradient.appendChild(
      createDownloadSvgElement('stop', {
        offset: '0%',
        'stop-color': '#7289da',
        'stop-opacity': '0'
      })
    );

    gradient.appendChild(
      createDownloadSvgElement('stop', {
        offset: '50%',
        'stop-color': '#7289da'
      })
    );

    gradient.appendChild(
      createDownloadSvgElement('stop', {
        offset: '100%',
        'stop-color': '#7289da',
        'stop-opacity': '0'
      })
    );

    defs.appendChild(gradient);
    markerSvg.appendChild(defs);

    markerSvg.appendChild(
      createDownloadSvgElement('path', {
        d: 'M4,4 L14.397,36 H105.602 L116,4 Z',
        fill: `url(#${gradientId})`,
        stroke: 'none'
      })
    );

    markerSvg.appendChild(
      createDownloadSvgElement('path', {
        d: 'M32.707,120 L60,36 L87.293,120',
        fill: 'none',
        stroke: '#7289da',
        'stroke-width': '4'
      })
    );
  }

  return markerSvg;
}

function getDownloadNumberMarkerPosition(corner = downloadNumberMarkerCorner) {
  const near = DOWNLOAD_NUMBER_MARGIN;

  const far =
    480 -
    DOWNLOAD_NUMBER_MARGIN -
    DOWNLOAD_NUMBER_MARKER_SIZE;

  return {
    x: corner.endsWith('r') ? far : near,
    y: corner.startsWith('b') ? far : near
  };
}

/*
 * 加入真正會被序列化及下載的記分標誌。
 * 它位於 SVG 根節點,不放進 #etanidrop,因此不受棋盤縮放影響。
 */
function appendDownloadNumberScoringMarker(svg) {
  const position = getDownloadNumberMarkerPosition();

  const marker = createDownloadScoringMarkerSvg({
    id: 'btnumberscoremarker',
    x: position.x,
    y: position.y,
    width: DOWNLOAD_NUMBER_MARKER_SIZE,
    height: DOWNLOAD_NUMBER_MARKER_SIZE,
    'pointer-events': 'none'
  });

  svg.appendChild(marker);
}

function updateDownloadMarkerPickerButton() {
  const button = document.getElementById('download-marker-position-btn');

  if (!button) {
    return;
  }

  const active =
    downloadMarkerPickerOpen &&
    getDownloadMode() === 'number';

  button.classList.toggle('active', active);
  button.setAttribute('aria-pressed', active ? 'true' : 'false');
}

function clearDownloadMarkerChoices() {
  const preview = document.getElementById('download-preview');

  if (!preview) {
    return;
  }

  preview.querySelectorAll('.download-marker-choice').forEach(choice => {
    choice.remove();
  });
}

/*
 * 候選標誌是 #download-preview 的 HTML 子元素,
 * 位於 object 上方,不會加入 object 內的 clone SVG。
 */
function renderDownloadMarkerChoices() {
  const preview = document.getElementById('download-preview');

  clearDownloadMarkerChoices();
  updateDownloadMarkerPickerButton();

  if (
    !preview ||
    !downloadMarkerPickerOpen ||
    getDownloadMode() !== 'number'
  ) {
    return;
  }

  ['tl', 'tr', 'bl', 'br']
    .filter(corner => corner !== downloadNumberMarkerCorner)
    .forEach(corner => {
      const choice = document.createElement('button');

      choice.type = 'button';
      choice.className = 'download-marker-choice';
      choice.dataset.corner = corner;
      choice.setAttribute('aria-label', `把記分標誌移到 ${corner}`);
      choice.title = '移動記分標誌';

      choice.addEventListener('click', event => {
        selectDownloadNumberMarkerCorner(corner, event);
      });

      choice.appendChild(
        createDownloadScoringMarkerSvg({
          width: '100%',
          height: '100%',
          focusable: 'false',
          'aria-hidden': 'true'
        })
      );

      preview.appendChild(choice);
    });
}

function setDownloadMarkerPickerOpen(open) {
  downloadMarkerPickerOpen =
    Boolean(open) &&
    getDownloadMode() === 'number';

  renderDownloadMarkerChoices();
}

function toggleDownloadMarkerPicker(event) {
  if (event) {
    event.preventDefault();
    event.stopPropagation();
  }

  const numberOption = document.getElementById('download-option-number');
  const animateOption = document.getElementById('download-option-animate');

  if (!numberOption) {
    return;
  }

  /*
   * 如果目前不是數字模式,點位置按鈕時自動切換到數字模式,
   * 並直接打開角落選擇。
   */
  if (!numberOption.checked) {
    numberOption.checked = true;

    if (animateOption) {
      animateOption.checked = false;
    }

    downloadMarkerPickerOpen = true;
    refreshDownloadPreview();
    return;
  }

  setDownloadMarkerPickerOpen(!downloadMarkerPickerOpen);
}

function handleDownloadMarkerPickerKeydown(event) {
  if (event.key !== 'Enter' && event.key !== ' ') {
    return;
  }

  toggleDownloadMarkerPicker(event);
}

function selectDownloadNumberMarkerCorner(corner, event) {
  if (event) {
    event.preventDefault();
    event.stopPropagation();
  }

  if (!DOWNLOAD_NUMBER_MARKER_CORNERS.has(corner)) {
    return;
  }

  downloadNumberMarkerCorner = corner;
  saveDownloadNumberMarkerCorner();

  /*
   * 選擇後關閉三個候選標誌,取消按鈕高亮,
   * 並重建 clone,讓真實記分標誌移到新位置。
   */
  downloadMarkerPickerOpen = false;
  refreshDownloadPreview();
}

五、替換下載選項切換函數

找到:

js
function handleDownloadOptionChange(changedMode) {
  const numberOption = document.getElementById('download-option-number');
  const animateOption = document.getElementById('download-option-animate');

  if (changedMode === 'number' && numberOption.checked) {
    animateOption.checked = false;
  }

  if (changedMode === 'animate' && animateOption.checked) {
    numberOption.checked = false;
  }

  refreshDownloadPreview();
}

替換成:

js
function handleDownloadOptionChange(changedMode) {
  const numberOption = document.getElementById('download-option-number');
  const animateOption = document.getElementById('download-option-animate');

  if (changedMode === 'number' && numberOption.checked) {
    animateOption.checked = false;
  }

  if (changedMode === 'animate' && animateOption.checked) {
    numberOption.checked = false;
  }

  /*
   * 直接切換數字/動畫/普通模式時,
   * 關閉角落候選標誌並取消位置按鈕高亮。
   */
  downloadMarkerPickerOpen = false;

  refreshDownloadPreview();
}

六、替換「數字模式」函數

找到完整的:

js
function addDownloadPieceNumbers(svg) {
  prependDownloadFontStyle(svg);

  svg.querySelectorAll('.board-marker-dot').forEach(dot => {
    dot.remove();
  });

  const drop = svg.querySelector('.etdrop');

  if (!drop) {
    return;
  }

  const uses = getDirectDownloadUses(drop);

  const modelPieces = pieces.concat(tempPieces, ghosts);

  uses.forEach((useElement, index) => {
    const number = index + 1;
    const piece = modelPieces[index] || null;
    const center = getDownloadPieceCenter(piece, useElement);

    const text = createDownloadSvgElement('text', {
      x: trimNum(center.x),
      y: trimNum(center.y),
      stroke: 'none',
      fill: '#333',
      'font-size': '18',
      'font-family': 'Techfont',
      'text-anchor': 'middle',
      'dominant-baseline': 'central'
    });

    text.textContent = String(number);
    drop.appendChild(text);
  });
}

替換成:

js
function addDownloadPieceNumbers(svg) {
  prependDownloadFontStyle(svg);

  /*
   * 數字導出不保留最後落子點、臨時棋子點及目標點。
   */
  svg.querySelectorAll('.board-marker-dot').forEach(dot => {
    dot.remove();
  });

  const drop = svg.querySelector('.etdrop');

  if (!drop) {
    appendDownloadNumberScoringMarker(svg);
    return;
  }

  const uses = getDirectDownloadUses(drop);
  const modelPieces = pieces.concat(tempPieces, ghosts);

  /*
   * 所有 text 最後加入 #etanidrop,
   * 確保數字顯示在棋子上方。
   */
  uses.forEach((useElement, index) => {
    const number = index + 1;
    const piece = modelPieces[index] || null;
    const center = getDownloadPieceCenter(piece, useElement);

    const text = createDownloadSvgElement('text', {
      x: trimNum(center.x),
      y: trimNum(center.y),
      stroke: 'none',
      fill: '#333',
      'font-size': '18',
      'font-family': 'Techfont',
      'text-anchor': 'middle',
      'dominant-baseline': 'central'
    });

    text.textContent = String(number);
    drop.appendChild(text);
  });

  /*
   * 套用和動畫最後全景相同的邊界計算邏輯。
   *
   * 數字模式沒有上方 60 高度的動畫狀態欄,
   * 因此四邊都保留 12 單位邊距。
   */
  const fullView = getDownloadFullView(modelPieces, {
    left: DOWNLOAD_NUMBER_MARGIN,
    top: DOWNLOAD_NUMBER_MARGIN,
    right: 480 - DOWNLOAD_NUMBER_MARGIN,
    bottom: 480 - DOWNLOAD_NUMBER_MARGIN
  });

  /*
   * 直接調整 clone 中真實的 g#etanidrop。
   * 棋子和數字會一起平移、縮放及居中。
   */
  drop.setAttribute(
    'transform',
    `translate(${trimNum(fullView.tx)},${trimNum(fullView.ty)}) ` +
      `scale(${trimNum(fullView.scale, 6)})`
  );

  /*
   * 記分標誌放在 SVG 根節點,固定於所選角落,
   * 不跟隨 #etanidrop 縮放。
   */
  appendDownloadNumberScoringMarker(svg);
}

七、替換動畫全景計算函數,使它也可供「數字」模式使用

找到完整的:

js
function getDownloadFullView(pieceRecords) {
  const points = [];

  pieceRecords.forEach(piece => {
    if (!piece || !piece.vertices) {
      return;
    }

    piece.vertices.forEach(vertex => {
      points.push(exactToScreen(vertex));
    });
  });

  if (points.length === 0) {
    return {
      tx: 240,
      ty: 270,
      scale: 1
    };
  }

  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  points.forEach(point => {
    minX = Math.min(minX, point.x);
    minY = Math.min(minY, point.y);
    maxX = Math.max(maxX, point.x);
    maxY = Math.max(maxY, point.y);
  });

  const width = Math.max(1, maxX - minX);
  const height = Math.max(1, maxY - minY);

  /*
   * 左右各保留 12:
   * 480 - 24 = 456
   *
   * 狀態欄下方高度為 420,上下各保留 12:
   * 420 - 24 = 396
   */
  const fullScale = Math.min(456 / width, 396 / height);

  const centerX = (minX + maxX) / 2;
  const centerY = (minY + maxY) / 2;

  return {
    tx: 240 - centerX * fullScale,
    ty: 270 - centerY * fullScale,
    scale: fullScale
  };
}

替換成:

js
/*
 * 計算所有棋子的全景鏡頭。
 *
 * 默認 viewport 保持原動畫模式:
 * x = 12..468
 * y = 72..468
 *
 * 即避開上方 60 高度的動畫狀態欄,並在棋子四周保留 12。
 *
 * 數字模式會傳入:
 * x = 12..468
 * y = 12..468
 */
function getDownloadFullView(
  pieceRecords,
  viewport = {
    left: 12,
    top: 72,
    right: 468,
    bottom: 468
  }
) {
  const points = [];

  pieceRecords.forEach(piece => {
    if (!piece || !piece.vertices) {
      return;
    }

    piece.vertices.forEach(vertex => {
      points.push(exactToScreen(vertex));
    });
  });

  const viewportWidth = Math.max(1, viewport.right - viewport.left);
  const viewportHeight = Math.max(1, viewport.bottom - viewport.top);

  const viewportCenterX = (viewport.left + viewport.right) / 2;
  const viewportCenterY = (viewport.top + viewport.bottom) / 2;

  if (points.length === 0) {
    return {
      tx: viewportCenterX,
      ty: viewportCenterY,
      scale: 1
    };
  }

  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;

  points.forEach(point => {
    minX = Math.min(minX, point.x);
    minY = Math.min(minY, point.y);
    maxX = Math.max(maxX, point.x);
    maxY = Math.max(maxY, point.y);
  });

  const width = Math.max(1, maxX - minX);
  const height = Math.max(1, maxY - minY);

  const fullScale = Math.min(
    viewportWidth / width,
    viewportHeight / height
  );

  const centerX = (minX + maxX) / 2;
  const centerY = (minY + maxY) / 2;

  return {
    tx: viewportCenterX - centerX * fullScale,
    ty: viewportCenterY - centerY * fullScale,
    scale: fullScale
  };
}

原本動畫中的這行:

js
const fullView = getDownloadFullView(actualPieceRecords);

不需要修改。因為新的默認參數仍然是動畫原先的:

  • 左右邊距 12
  • 上方狀態欄 60
  • 狀態欄下再留 12
  • 底部邊距 12
  • 中心仍然是 (240, 270)

八、讓預覽刷新後顯示三個候選標誌

refreshDownloadPreview() 中找到:

js
preview.appendChild(object);

requestAnimationFrame(fitDownloadPreview);

替換成:

js
preview.appendChild(object);

/*
 * 候選標誌加入預覽容器,而不是加入 object 內的 SVG。
 */
renderDownloadMarkerChoices();

requestAnimationFrame(fitDownloadPreview);

完整位置應該是:

js
const object = document.createElement('object');

object.type = 'image/svg+xml';
object.data = downloadPreviewUrl;
object.setAttribute('aria-label', '下載 SVG 預覽');

preview.appendChild(object);

/*
 * 候選標誌加入預覽容器,而不是加入 object 內的 SVG。
 */
renderDownloadMarkerChoices();

requestAnimationFrame(fitDownloadPreview);

九、關閉下載窗口時取消高亮和候選標誌

closeDownloadDialog() 中找到:

js
dialog.style.display = 'none';

if (preview) {

替換成:

js
dialog.style.display = 'none';

downloadMarkerPickerOpen = false;
updateDownloadMarkerPickerButton();

if (preview) {

修改後完整函數為:

js
function closeDownloadDialog() {
  const dialog = document.getElementById('download-dialog');
  const preview = document.getElementById('download-preview');

  dialog.style.display = 'none';

  downloadMarkerPickerOpen = false;
  updateDownloadMarkerPickerButton();

  if (preview) {
    preview.replaceChildren();
    preview.style.width = '';
    preview.style.height = '';
  }

  releaseDownloadPreviewUrl();

  downloadSvgSource = '';
  downloadSvgBlob = null;
}

修改後的行為

  1. 選中「數字」:

    • clone 中的 g#etanidrop 自動修改為全景 translate + scale
    • 所有正式棋子、臨時棋子和目前顯示的 ghost 都會納入邊界計算。
    • 棋子整體在 480×480 視圖中居中,四周保留 12 單位邊距。
    • 右下角加入真正的 48×48 記分標誌。
  2. 點擊「數字」右側的四角按鈕:

    • 按鈕高亮。
    • 除真實記分標誌所在角落外,其餘三個角落顯示透明度 0.777 的候選標誌。
    • 三個候選標誌位於 #download-preview 上方,不會序列化到下載 SVG。
  3. 點擊任意候選標誌:

    • 保存新角落。
    • 重建下載 SVG。
    • 真實的 #btnumberscoremarker 移到新位置。
    • 三個候選標誌消失。
    • 四角按鈕取消高亮。
  4. 再次點擊已高亮的四角按鈕:

    • 直接關閉候選標誌並恢復非高亮狀態。
  5. 記分標誌位置:

    • 使用 localStorage 保存。
    • 下次打開「數字」導出時仍使用上次選擇的位置。