{"id":12696,"date":"2026-02-09T14:31:46","date_gmt":"2026-02-09T14:31:46","guid":{"rendered":"https:\/\/top30.com.sg\/a-primer-on-pairing-en\/"},"modified":"2026-09-01T09:31:00","modified_gmt":"2026-09-01T09:31:00","slug":"a-primer-on-pairing-en","status":"publish","type":"page","link":"https:\/\/top30.com.sg\/zh-hans\/a-primer-on-pairing-en\/","title":{"rendered":"A Primer on Pairing"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">&nbsp;<\/p>\n\n\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes\">\n<title>A Primer on Pairing<\/title>\n<style>\n  * { box-sizing: border-box; margin: 0; padding: 0; }\n  html, body {\n    height: 100%;\n    font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n    background: #fff;\n    color: #1a1d27;\n  }\n  .app {\n    display: grid;\n    grid-template-columns: 140px 1fr;\n    grid-template-rows: auto 1fr;\n    height: 100vh;\n    height: 100dvh;\n  }\n  .toolbar {\n    grid-column: 1 \/ -1;\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: 8px;\n    padding: 10px 12px;\n    background: #f7f8fa;\n    border-bottom: 1px solid #e2e5eb;\n    z-index: 20;\n  }\n\n  .btn {\n    height: 42px;\n    min-width: 42px;\n    padding: 0 14px;\n    border: none;\n    border-radius: 999px;\n    background: #ff8808;\n    color: #fff;\n    font-size: 18px;\n    font-weight: 700;\n    cursor: pointer;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    text-decoration: none;\n    user-select: none;\n  }\n  .btn:hover, .btn:active { background: #e67700; }\n  .btn:disabled { opacity: 0.4; pointer-events: none; }\n\n  .spacer { flex: 1; min-width: 8px; }\n  .page-controls, .zoom-controls {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n  }\n\n  .page-input, .zoom-input {\n    height: 42px;\n    min-width: 90px;\n    padding: 0 12px;\n    text-align: center;\n    border: 2px solid #ff8808;\n    border-radius: 999px;\n    font-size: 18px;\n    font-weight: 700;\n    line-height: 38px;\n    background: #ffffff;\n    color: #111111 !important;\n    -webkit-text-fill-color: #111111;\n    appearance: textfield;\n    -moz-appearance: textfield;\n  }\n  .page-input { width: 90px; }\n  .zoom-input { width: 100px; }\n\n  .page-input::-webkit-outer-spin-button,\n  .page-input::-webkit-inner-spin-button,\n  .zoom-input::-webkit-outer-spin-button,\n  .zoom-input::-webkit-inner-spin-button {\n    -webkit-appearance: none;\n    margin: 0;\n    display: none;\n  }\n\n  .page-input:focus, .zoom-input:focus {\n    outline: none;\n    box-shadow: 0 0 0 3px rgba(255, 136, 8, 0.3);\n  }\n\n  .page-total {\n    color: #333;\n    font-size: 16px;\n    font-weight: 700;\n    white-space: nowrap;\n  }\n\n  .sidebar {\n    border-right: 1px solid #e2e5eb;\n    background: #f7f8fa;\n    overflow-y: auto;\n    overflow-x: hidden;\n    padding: 10px 8px;\n    -webkit-overflow-scrolling: touch;\n  }\n  .sidebar h4 {\n    font-size: 11px;\n    text-transform: uppercase;\n    color: #6b7280;\n    letter-spacing: 0.04em;\n    margin-bottom: 8px;\n  }\n  .thumbs { display: flex; flex-direction: column; gap: 6px; }\n  .thumb {\n    position: relative;\n    border: 2px solid transparent;\n    border-radius: 6px;\n    overflow: hidden;\n    cursor: pointer;\n    background: #eef0f4;\n    aspect-ratio: 419 \/ 595;\n    flex-shrink: 0;\n  }\n  .thumb:hover { border-color: #c5cad3; }\n  .thumb.active { border-color: #ff8808; box-shadow: 0 0 0 1px #ff8808; }\n  .thumb canvas { width: 100%; height: 100%; display: block; }\n  .thumb .num {\n    position: absolute; bottom: 2px; right: 3px;\n    background: rgba(0,0,0,.7); color: #fff;\n    font-size: 10px; padding: 1px 4px; border-radius: 3px;\n  }\n  .stage {\n    position: relative;\n    overflow: auto;\n    background: #f0f2f5;\n    display: flex;\n    justify-content: center;\n    align-items: flex-start;\n    padding: 16px;\n    -webkit-overflow-scrolling: touch;\n  }\n  #pageCanvas {\n    box-shadow: 0 4px 20px rgba(0,0,0,.12);\n    background: #fff;\n    display: block;\n  }\n  .overlay {\n    position: absolute; inset: 0;\n    display: flex; flex-direction: column;\n    align-items: center; justify-content: center;\n    gap: 14px; background: #fff; z-index: 5;\n    text-align: center; padding: 24px;\n  }\n  .overlay.hidden { display: none; }\n  .spinner {\n    width: 36px; height: 36px;\n    border: 3px solid #e5e7eb; border-top-color: #ff8808;\n    border-radius: 50%; animation: spin .7s linear infinite;\n  }\n  @keyframes spin { to { transform: rotate(360deg); } }\n  .overlay p { max-width: 340px; line-height: 1.5; color: #6b7280; font-size: 14px; }\n  .overlay code {\n    background: #f3f4f6; padding: 2px 6px; border-radius: 4px;\n    font-size: 12px; word-break: break-all;\n  }\n  @media (max-width: 768px) {\n    .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }\n    .sidebar {\n      grid-row: 3; border-right: none; border-top: 1px solid #e2e5eb;\n      height: 100px; padding: 8px; overflow-x: auto; overflow-y: hidden;\n    }\n    .sidebar h4 { display: none; }\n    .thumbs { flex-direction: row; height: 100%; }\n    .thumb { width: 52px; min-width: 52px; height: 74px; aspect-ratio: auto; }\n    .stage { padding: 10px; }\n    .page-input { width: 80px; min-width: 80px; }\n    .zoom-input { width: 90px; min-width: 90px; }\n  }\n<\/style>\n<\/head>\n<body>\n<div class=\"app\">\n  <header class=\"toolbar\">\n    <div class=\"page-controls\">\n      <button class=\"btn\" id=\"prevBtn\" title=\"Previous\" aria-label=\"Previous\">\u2039<\/button>\n      <input type=\"text\" class=\"page-input\" id=\"pageInput\" value=\"1\" inputmode=\"numeric\" pattern=\"[0-9]*\" aria-label=\"Page number\">\n      <span class=\"page-total\" id=\"pageTotal\">\/ \u2013<\/span>\n      <button class=\"btn\" id=\"nextBtn\" title=\"Next\" aria-label=\"Next\">\u203a<\/button>\n    <\/div>\n    <div class=\"spacer\"><\/div>\n    <div class=\"zoom-controls\">\n      <button class=\"btn\" id=\"zoomOutBtn\" title=\"Zoom out\">\u2212<\/button>\n      <input type=\"text\" class=\"zoom-input\" id=\"zoomInput\" value=\"100\" inputmode=\"numeric\" pattern=\"[0-9]*\" aria-label=\"Zoom percent\">\n      <span class=\"page-total\">%<\/span>\n      <button class=\"btn\" id=\"zoomInBtn\" title=\"Zoom in\">+<\/button>\n    <\/div>\n    <a class=\"btn\" id=\"downloadBtn\"\n       href=\"https:\/\/top30.com.sg\/wp-content\/uploads\/2026\/03\/A-Primer-on-Pairing_SCHN_compressed.pdf\"\n       download target=\"_blank\" rel=\"noopener\">\u4e0b\u8f7d<\/a>\n  <\/header>\n\n  <aside class=\"sidebar\">\n    <h4>Pages<\/h4>\n    <div class=\"thumbs\" id=\"thumbs\"><\/div>\n  <\/aside>\n\n  <main class=\"stage\" id=\"stage\">\n    <canvas id=\"pageCanvas\"><\/canvas>\n    <div class=\"overlay\" id=\"loading\">\n      <div class=\"spinner\"><\/div>\n      <div>Loading PDF\u2026<\/div>\n    <\/div>\n    <div class=\"overlay hidden\" id=\"errorBox\">\n      <strong style=\"font-size:16px\">Could not load the PDF<\/strong>\n      <p id=\"errorMsg\"><\/p>\n    <\/div>\n  <\/main>\n<\/div>\n\n<script type=\"module\">\n  import * as pdfjsLib from 'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/4.10.38\/pdf.min.mjs';\n  pdfjsLib.GlobalWorkerOptions.workerSrc =\n    'https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pdf.js\/4.10.38\/pdf.worker.min.mjs';\n\n  \/\/ const PDF_URL = '.\/A-Primer-on-Pairing_SCHN_compressed.pdf';\n  const PDF_URL = 'https:\/\/top30.com.sg\/wp-content\/uploads\/2026\/03\/A-Primer-on-Pairing_SCHN_compressed.pdf';\n\n  let pdfDoc = null;\n  let currentPage = 1;\n  let totalPages = 0;\n  let scale = 1;\n  let renderToken = 0;\n\n  const canvas = document.getElementById('pageCanvas');\n  const ctx = canvas.getContext('2d');\n  const loading = document.getElementById('loading');\n  const errorBox = document.getElementById('errorBox');\n  const errorMsg = document.getElementById('errorMsg');\n  const pageInput = document.getElementById('pageInput');\n  const pageTotal = document.getElementById('pageTotal');\n  const zoomInput = document.getElementById('zoomInput');\n  const thumbsEl = document.getElementById('thumbs');\n  const stage = document.getElementById('stage');\n  const prevBtn = document.getElementById('prevBtn');\n  const nextBtn = document.getElementById('nextBtn');\n\n  function showError(html) {\n    loading.classList.add('hidden');\n    errorBox.classList.remove('hidden');\n    errorMsg.innerHTML = html;\n  }\n\n  if (location.protocol === 'file:') {\n    showError(\n      'Open via a local server:<br><br>' +\n      '<code>python3 -m http.server 8080<\/code><br><br>' +\n      'Then open <code>http:\/\/localhost:8080\/yourfile.html<\/code>'\n    );\n  } else {\n    loadPdf();\n  }\n\n  async function loadPdf() {\n    try {\n      pdfDoc = await pdfjsLib.getDocument({ url: PDF_URL, withCredentials: false }).promise;\n      totalPages = pdfDoc.numPages;\n      pageTotal.textContent = '\/ ' + totalPages;\n      loading.classList.add('hidden');\n      await renderPage(1);\n      setTimeout(generateThumbnails, 300);\n    } catch (err) {\n      console.error(err);\n      const msg = err?.message || String(err);\n      if (\/cors|cross-origin|network|failed to fetch\/i.test(msg)) {\n        showError(\n          'CORS blocked the remote PDF.<br><br>' +\n          'Use a local file:<br><br>' +\n          '<code>const PDF_URL = \\'.\/A-Primer-on-Pairing_SCHN_compressed.pdf\\';<\/code>'\n        );\n      } else {\n        showError('Error: ' + msg);\n      }\n    }\n  }\n\n  function calcBaseScale(page) {\n    const padding = window.innerWidth <= 768 ? 24 : 48;\n    const avail = Math.max(240, stage.clientWidth - padding);\n    return (avail \/ page.getViewport({ scale: 1 }).width) * 0.9;\n  }\n\n  async function renderPage(num) {\n    if (!pdfDoc) return;\n    const token = ++renderToken;\n    try {\n      const page = await pdfDoc.getPage(num);\n      if (token !== renderToken) return;\n\n      const baseScale = calcBaseScale(page);\n      const viewport = page.getViewport({ scale: baseScale * scale });\n\n      canvas.width = Math.floor(viewport.width);\n      canvas.height = Math.floor(viewport.height);\n      canvas.style.width = Math.floor(viewport.width) + 'px';\n      canvas.style.height = Math.floor(viewport.height) + 'px';\n\n      await page.render({\n        canvasContext: ctx,\n        viewport,\n        intent: 'display',\n        annotationMode: 0\n      }).promise;\n\n      if (token !== renderToken) return;\n\n      currentPage = num;\n      pageInput.value = String(num);\n      prevBtn.disabled = num <= 1;\n      nextBtn.disabled = num >= totalPages;\n      zoomInput.value = String(Math.round(scale * 100));\n      highlightThumb(num);\n    } catch (e) {\n      console.error(e);\n    }\n  }\n\n  async function generateThumbnails() {\n    for (let i = 1; i <= totalPages; i++) {\n      const div = document.createElement('div');\n      div.className = 'thumb' + (i === 1 ? ' active' : '');\n      div.dataset.page = i;\n      div.title = 'Page ' + i;\n      const c = document.createElement('canvas');\n      div.appendChild(c);\n      const badge = document.createElement('span');\n      badge.className = 'num';\n      badge.textContent = i;\n      div.appendChild(badge);\n      div.addEventListener('click', () => goTo(i));\n      thumbsEl.appendChild(div);\n    }\n    for (let i = 1; i <= totalPages; i++) {\n      try {\n        const page = await pdfDoc.getPage(i);\n        const vp = page.getViewport({ scale: 0.12 });\n        const el = thumbsEl.querySelector(`.thumb[data-page=\"${i}\"] canvas`);\n        if (!el) continue;\n        el.width = vp.width;\n        el.height = vp.height;\n        await page.render({ canvasContext: el.getContext('2d'), viewport: vp }).promise;\n      } catch (e) {}\n      if (i % 6 === 0) await new Promise(r => setTimeout(r, 0));\n    }\n  }\n\n  function highlightThumb(page) {\n    thumbsEl.querySelectorAll('.thumb').forEach(el => {\n      el.classList.toggle('active', parseInt(el.dataset.page, 10) === page);\n    });\n    const active = thumbsEl.querySelector(`.thumb[data-page=\"${page}\"]`);\n    if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });\n  }\n\n  function goTo(n) {\n    n = Math.max(1, Math.min(totalPages || 1, parseInt(n, 10) || 1));\n    renderPage(n);\n  }\n\n  function setZoom(percent) {\n    let p = parseFloat(percent);\n    if (isNaN(p)) p = 100;\n    p = Math.max(50, Math.min(250, p));\n    scale = p \/ 100;\n    zoomInput.value = String(Math.round(p));\n    renderPage(currentPage);\n  }\n\n  prevBtn.addEventListener('click', () => goTo(currentPage - 1));\n  nextBtn.addEventListener('click', () => goTo(currentPage + 1));\n  document.getElementById('zoomInBtn').addEventListener('click', () => setZoom(scale * 100 + 25));\n  document.getElementById('zoomOutBtn').addEventListener('click', () => setZoom(scale * 100 - 25));\n\n  pageInput.addEventListener('change', () => goTo(pageInput.value));\n  pageInput.addEventListener('keydown', (e) => {\n    if (e.key === 'Enter') { e.preventDefault(); goTo(pageInput.value); pageInput.blur(); }\n  });\n  zoomInput.addEventListener('change', () => setZoom(zoomInput.value));\n  zoomInput.addEventListener('keydown', (e) => {\n    if (e.key === 'Enter') { e.preventDefault(); setZoom(zoomInput.value); zoomInput.blur(); }\n  });\n\n  document.addEventListener('keydown', (e) => {\n    if (e.target.matches('input, textarea')) return;\n    if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goTo(currentPage + 1); }\n    if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(currentPage - 1); }\n    if (e.key === '+' || e.key === '=') setZoom(scale * 100 + 25);\n    if (e.key === '-') setZoom(scale * 100 - 25);\n  });\n\n  let resizeTimer;\n  window.addEventListener('resize', () => {\n    clearTimeout(resizeTimer);\n    resizeTimer = setTimeout(() => { if (pdfDoc) renderPage(currentPage); }, 150);\n  });\n<\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>&nbsp; A Primer on Pairing \u2039 \/ \u2013 \u203a \u2212 % + \u4e0b\u8f7d Pages Loadi [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-12696","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/pages\/12696","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/comments?post=12696"}],"version-history":[{"count":18,"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/pages\/12696\/revisions"}],"predecessor-version":[{"id":13973,"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/pages\/12696\/revisions\/13973"}],"wp:attachment":[{"href":"https:\/\/top30.com.sg\/zh-hans\/wp-json\/wp\/v2\/media?parent=12696"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}