{"id":11938,"date":"2026-03-13T17:32:04","date_gmt":"2026-03-13T16:32:04","guid":{"rendered":"https:\/\/spgoo.org\/?page_id=11938"},"modified":"2026-03-13T17:32:04","modified_gmt":"2026-03-13T16:32:04","slug":"signature-fourier","status":"publish","type":"page","link":"https:\/\/spgoo.org\/?page_id=11938","title":{"rendered":"Signature-Fourier"},"content":{"rendered":"<style>\r\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\r\n\/*  body {\r\n    font-family: system-ui, sans-serif;\r\n    background: #f4f5f8;\r\n    color: #111;\r\n    min-height: 100vh;\r\n    padding: 1.5rem 2rem 2rem;\r\n  }*\/\r\n  h1 { font-size: 17px; font-weight: 500; margin-bottom: 1rem; color: #111; letter-spacing: -.01em; }\r\n\r\n  .controls {\r\n    display: flex; align-items: center; gap: 10px;\r\n    flex-wrap: wrap; margin-bottom: 1rem;\r\n    background: #fff; border: 0.5px solid rgba(0,0,0,.11);\r\n    border-radius: 12px; padding: 10px 16px;\r\n  }\r\n  .ctrl-group { display: flex; align-items: center; gap: 7px; }\r\n  label { font-size: 12.5px; color: #555; white-space: nowrap; }\r\n  select, button {\r\n    font-size: 12.5px; padding: 5px 10px; border-radius: 7px;\r\n    border: 0.5px solid rgba(0,0,0,.18); background: #fff; cursor: pointer; color: #111;\r\n  }\r\n  button:hover { background: #f0f0f0; }\r\n  input[type=range] { width: 80px; cursor: pointer; accent-color: #185fa5; }\r\n  .val { font-size: 12.5px; font-weight: 500; min-width: 24px; color: #111; font-family: monospace; }\r\n  .sep { width: 0.5px; height: 22px; background: rgba(0,0,0,.11); }\r\n\r\n  .panels {\r\n    display: grid;\r\n    grid-template-columns: repeat(3, 1fr);\r\n    gap: 14px;\r\n  }\r\n  .panel {\r\n    background: #fff; border: 0.5px solid rgba(0,0,0,.11);\r\n    border-radius: 12px; padding: 14px;\r\n  }\r\n  .panel-title {\r\n    font-size: 11px; color: #999; margin-bottom: 10px;\r\n    letter-spacing: .05em; text-transform: uppercase; font-weight: 500;\r\n  }\r\n  .panel-title em { color: #185fa5; font-style: normal; }\r\n  canvas { display: block; width: 100%; aspect-ratio: 1; border-radius: 6px; }\r\n\r\n  .fft-legend {\r\n    display: flex; gap: 14px; margin-top: 8px; font-size: 11px; color: #888; flex-wrap: wrap;\r\n  }\r\n  .fft-legend span { display: flex; align-items: center; gap: 5px; }\r\n  .leg-line { width: 16px; height: 2px; display: inline-block; border-radius: 1px; }\r\n\r\n  .info-row { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }\r\n  .info-item {\r\n    flex: 1; min-width: 90px; background: #fff;\r\n    border: 0.5px solid rgba(0,0,0,.1); border-radius: 8px; padding: 7px 11px;\r\n  }\r\n  .info-label { font-size: 10.5px; color: #999; margin-bottom: 2px; text-transform: uppercase; letter-spacing: .04em; }\r\n  .info-val { font-size: 13px; font-weight: 500; color: #111; font-family: monospace; }\r\n\r\n  .btn-row { display: flex; gap: 7px; }\r\n\r\n  @media (max-width: 820px) {\r\n    .panels { grid-template-columns: 1fr 1fr; }\r\n    body { padding: 1rem; }\r\n  }\r\n  @media (max-width: 520px) {\r\n    .panels { grid-template-columns: 1fr; }\r\n  }\r\n<\/style>\r\n\r\n<h1>Shape Path Tracer \u2014 Position &#038; Transform\u00e9e de Fourier<\/h1>\r\n\r\n<div class=\"controls\">\r\n  <div class=\"ctrl-group\">\r\n    <label>Forme<\/label>\r\n    <select id=\"shapeSelect\">\r\n      <option value=\"square\">Carr\u00e9<\/option>\r\n      <option value=\"rectangle\">Rectangle<\/option>\r\n      <option value=\"circle\">Cercle<\/option>\r\n    <\/select>\r\n  <\/div>\r\n  <div class=\"sep\"><\/div>\r\n  <div class=\"ctrl-group\">\r\n    <label>Vitesse<\/label>\r\n    <input type=\"range\" id=\"speedSlider\" min=\"1\" max=\"10\" value=\"4\" step=\"1\">\r\n    <span class=\"val\" id=\"speedVal\">4<\/span>\r\n  <\/div>\r\n  <div class=\"sep\"><\/div>\r\n  <div class=\"ctrl-group\">\r\n    <label>Tra\u00een\u00e9e<\/label>\r\n    <input type=\"range\" id=\"trailSlider\" min=\"0\" max=\"60\" value=\"30\" step=\"1\">\r\n    <span class=\"val\" id=\"trailVal\">30<\/span>\r\n  <\/div>\r\n  <div class=\"sep\"><\/div>\r\n  <div class=\"ctrl-group\">\r\n    <label>Harmoniques<\/label>\r\n    <input type=\"range\" id=\"fftNSlider\" min=\"4\" max=\"32\" value=\"16\" step=\"1\">\r\n    <span class=\"val\" id=\"fftNVal\">16<\/span>\r\n  <\/div>\r\n  <div class=\"sep\"><\/div>\r\n  <div class=\"btn-row\">\r\n    <button id=\"btnPlayPause\">Pause<\/button>\r\n    <button id=\"btnReset\">R\u00e9initialiser<\/button>\r\n  <\/div>\r\n<\/div>\r\n\r\n<div class=\"panels\">\r\n  <div class=\"panel\">\r\n    <div class=\"panel-title\">Forme \u2014 parcours en temps r\u00e9el<\/div>\r\n    <canvas id=\"cvShape\"><\/canvas>\r\n  <\/div>\r\n  <div class=\"panel\">\r\n    <div class=\"panel-title\">Position X &#038; Y dans le temps<\/div>\r\n    <canvas id=\"cvTime\"><\/canvas>\r\n  <\/div>\r\n  <div class=\"panel\">\r\n    <div class=\"panel-title\">Transform\u00e9e de Fourier \u2014 <em>spectre d&#8217;amplitude &#038; phase<\/em><\/div>\r\n    <canvas id=\"cvFFT\"><\/canvas>\r\n    <div class=\"fft-legend\">\r\n      <span><span class=\"leg-line\" style=\"background:#185fa5\"><\/span>|X(k)|<\/span>\r\n      <span><span class=\"leg-line\" style=\"background:#1d9e75\"><\/span>|Y(k)|<\/span>\r\n      <span><span class=\"leg-line\" style=\"background:#ba7517;border-top:2px dashed #ba7517;height:0\"><\/span>phase X<\/span>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<div class=\"info-row\">\r\n  <div class=\"info-item\"><div class=\"info-label\">Progression t<\/div><div class=\"info-val\" id=\"infoT\">0.000<\/div><\/div>\r\n  <div class=\"info-item\"><div class=\"info-label\">Position X<\/div><div class=\"info-val\" id=\"infoPx\">0<\/div><\/div>\r\n  <div class=\"info-item\"><div class=\"info-label\">Position Y<\/div><div class=\"info-val\" id=\"infoPy\">0<\/div><\/div>\r\n  <div class=\"info-item\"><div class=\"info-label\">Cycle<\/div><div class=\"info-val\" id=\"infoCycle\">1<\/div><\/div>\r\n  <div class=\"info-item\"><div class=\"info-label\">Freq. dom. X<\/div><div class=\"info-val\" id=\"infoDomX\">\u2014<\/div><\/div>\r\n  <div class=\"info-item\"><div class=\"info-label\">Freq. dom. Y<\/div><div class=\"info-val\" id=\"infoDomY\">\u2014<\/div><\/div>\r\n<\/div>\r\n\r\n<script>\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   CANVAS SETUP\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nconst cvShape = document.getElementById('cvShape');\r\nconst cvTime  = document.getElementById('cvTime');\r\nconst cvFFT   = document.getElementById('cvFFT');\r\nconst ctxS = cvShape.getContext('2d');\r\nconst ctxT = cvTime.getContext('2d');\r\nconst ctxF = cvFFT.getContext('2d');\r\n\r\nconst shapeSelect = document.getElementById('shapeSelect');\r\nconst speedSlider = document.getElementById('speedSlider');\r\nconst trailSlider = document.getElementById('trailSlider');\r\nconst fftNSlider  = document.getElementById('fftNSlider');\r\nconst speedVal    = document.getElementById('speedVal');\r\nconst trailVal    = document.getElementById('trailVal');\r\nconst fftNVal     = document.getElementById('fftNVal');\r\nconst btnPlayPause= document.getElementById('btnPlayPause');\r\nconst btnReset    = document.getElementById('btnReset');\r\nconst infoT       = document.getElementById('infoT');\r\nconst infoPx      = document.getElementById('infoPx');\r\nconst infoPy      = document.getElementById('infoPy');\r\nconst infoCycle   = document.getElementById('infoCycle');\r\nconst infoDomX    = document.getElementById('infoDomX');\r\nconst infoDomY    = document.getElementById('infoDomY');\r\n\r\nconst CS = 420;\r\n[cvShape, cvTime, cvFFT].forEach(c => { c.width = c.height = CS; });\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   \u00c9TAT\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nlet t = 0, paused = false, cycle = 1, lastTS = 0;\r\nlet trailPoints  = [];\r\nlet timeHistory  = [];\r\nlet fftCache     = null;\r\nlet fftTick      = 0;\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   COULEURS\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nconst C = {\r\n  shape : '#185fa5',\r\n  accent: '#1d9e75',\r\n  amber : '#ba7517',\r\n  bg    : '#ffffff',\r\n  border: 'rgba(0,0,0,0.10)',\r\n  grid  : 'rgba(0,0,0,0.055)',\r\n  text1 : '#555',\r\n  text2 : '#aaa',\r\n};\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   G\u00c9OM\u00c9TRIE\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction getShape() {\r\n  const s = shapeSelect.value;\r\n  const M = 60, W = CS - M*2, H = CS - M*2;\r\n  if (s === 'square') {\r\n    const side = Math.min(W, H);\r\n    const ox = (CS-side)\/2, oy = (CS-side)\/2;\r\n    return { type:'poly', pts:[[ox,oy],[ox+side,oy],[ox+side,oy+side],[ox,oy+side]] };\r\n  }\r\n  if (s === 'rectangle') {\r\n    const rw = W, rh = H*0.52;\r\n    const ox = (CS-rw)\/2, oy = (CS-rh)\/2;\r\n    return { type:'poly', pts:[[ox,oy],[ox+rw,oy],[ox+rw,oy+rh],[ox,oy+rh]] };\r\n  }\r\n  return { type:'circle', cx:CS\/2, cy:CS\/2, r:Math.min(W,H)\/2 };\r\n}\r\n\r\nfunction posOnShape(shape, frac) {\r\n  frac = ((frac%1)+1)%1;\r\n  if (shape.type === 'circle') {\r\n    const a = -Math.PI\/2 + frac*2*Math.PI;\r\n    return [shape.cx + shape.r*Math.cos(a), shape.cy + shape.r*Math.sin(a)];\r\n  }\r\n  const pts = shape.pts;\r\n  const segs = pts.map((p,i) => {\r\n    const q = pts[(i+1)%pts.length];\r\n    return Math.hypot(q[0]-p[0], q[1]-p[1]);\r\n  });\r\n  const total = segs.reduce((a,b)=>a+b, 0);\r\n  let dist = frac * total;\r\n  for (let i=0; i<pts.length; i++) {\r\n    if (dist <= segs[i]) {\r\n      const a=pts[i], b=pts[(i+1)%pts.length], u=dist\/segs[i];\r\n      return [a[0]+(b[0]-a[0])*u, a[1]+(b[1]-a[1])*u];\r\n    }\r\n    dist -= segs[i];\r\n  }\r\n  return pts[0];\r\n}\r\n\r\nfunction normPos(shape, pos) {\r\n  if (shape.type === 'circle') {\r\n    return {\r\n      nx: (pos[0]-shape.cx+shape.r)\/(2*shape.r),\r\n      ny: (pos[1]-shape.cy+shape.r)\/(2*shape.r)\r\n    };\r\n  }\r\n  const xs = shape.pts.map(p=>p[0]), ys = shape.pts.map(p=>p[1]);\r\n  const mnX=Math.min(...xs), mxX=Math.max(...xs);\r\n  const mnY=Math.min(...ys), mxY=Math.max(...ys);\r\n  return {\r\n    nx: (pos[0]-mnX)\/(mxX-mnX)||0,\r\n    ny: (pos[1]-mnY)\/(mxY-mnY)||0\r\n  };\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   DFT \u2014 Transform\u00e9e de Fourier Discr\u00e8te (JS natif)\r\n   Calcule les coefficients complexes pour k = 0..N\/2\r\n   Retourne: tableau de { k, amp, phase, re, im }\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction dft(signal) {\r\n  const N = signal.length;\r\n  if (N < 4) return [];\r\n  const mean = signal.reduce((a,b)=>a+b,0)\/N;\r\n  const s = signal.map(v => v - mean);\r\n  const result = [];\r\n  const halfN = Math.floor(N\/2);\r\n  for (let k=0; k<=halfN; k++) {\r\n    let re=0, im=0;\r\n    const w = (2*Math.PI*k)\/N;\r\n    for (let n=0; n<N; n++) {\r\n      re += s[n]*Math.cos(w*n);\r\n      im -= s[n]*Math.sin(w*n);\r\n    }\r\n    re \/= N; im \/= N;\r\n    const amp   = k===0 ? Math.abs(re) : 2*Math.hypot(re,im);\r\n    const phase = Math.atan2(im, re);\r\n    result.push({ k, amp, phase, re, im });\r\n  }\r\n  return result;\r\n}\r\n\r\n\/* R\u00e9\u00e9chantillonner l'historique \u00e0 N points uniformes sur [0,1] *\/\r\nfunction resampleUniform(history, N) {\r\n  if (history.length < 4) return null;\r\n  const xs = new Float64Array(N);\r\n  const ys = new Float64Array(N);\r\n  for (let i=0; i<N; i++) {\r\n    const frac = i\/N;\r\n    let lo=0, hi=history.length-1;\r\n    while (lo < hi-1) {\r\n      const mid=(lo+hi)>>1;\r\n      history[mid].t <= frac ? lo=mid : hi=mid;\r\n    }\r\n    const a=history[lo], b=history[hi];\r\n    const u = a.t===b.t ? 0 : (frac-a.t)\/(b.t-a.t);\r\n    xs[i] = a.nx + (b.nx-a.nx)*Math.max(0,Math.min(1,u));\r\n    ys[i] = a.ny + (b.ny-a.ny)*Math.max(0,Math.min(1,u));\r\n  }\r\n  return { xs, ys };\r\n}\r\n\r\nfunction computeFFT() {\r\n  if (timeHistory.length < 16) return null;\r\n  const N = Math.min(512, Math.max(64, timeHistory.length));\r\n  const samples = resampleUniform(timeHistory, N);\r\n  if (!samples) return null;\r\n  return {\r\n    fX: dft(Array.from(samples.xs)),\r\n    fY: dft(Array.from(samples.ys))\r\n  };\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   RENDU 1 \u2014 FORME\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction renderShape(shape, pos, trail) {\r\n  const ctx = ctxS;\r\n  ctx.clearRect(0,0,CS,CS);\r\n  ctx.fillStyle = C.bg; ctx.fillRect(0,0,CS,CS);\r\n\r\n  \/\/ Contour fant\u00f4me\r\n  ctx.beginPath();\r\n  if (shape.type==='circle') {\r\n    ctx.arc(shape.cx,shape.cy,shape.r,0,2*Math.PI);\r\n  } else {\r\n    const pts=shape.pts;\r\n    ctx.moveTo(pts[0][0],pts[0][1]);\r\n    pts.slice(1).forEach(p=>ctx.lineTo(p[0],p[1]));\r\n    ctx.closePath();\r\n  }\r\n  ctx.strokeStyle=C.border; ctx.lineWidth=1; ctx.stroke();\r\n\r\n  \/\/ Trait parcouru\r\n  ctx.beginPath();\r\n  if (shape.type==='circle') {\r\n    ctx.arc(shape.cx,shape.cy,shape.r,-Math.PI\/2,-Math.PI\/2+t*2*Math.PI);\r\n  } else {\r\n    const pts=shape.pts;\r\n    const segs=pts.map((p,i)=>Math.hypot(...[0,1].map(d=>pts[(i+1)%pts.length][d]-p[d])));\r\n    const total=segs.reduce((a,b)=>a+b,0);\r\n    let rem=t*total;\r\n    ctx.moveTo(pts[0][0],pts[0][1]);\r\n    for (let i=0;i<pts.length;i++) {\r\n      if (rem<=0) break;\r\n      const a=pts[i],b=pts[(i+1)%pts.length];\r\n      if (rem>=segs[i]) { ctx.lineTo(b[0],b[1]); rem-=segs[i]; }\r\n      else { const u=rem\/segs[i]; ctx.lineTo(a[0]+(b[0]-a[0])*u,a[1]+(b[1]-a[1])*u); rem=0; }\r\n    }\r\n  }\r\n  ctx.strokeStyle=C.shape; ctx.lineWidth=2.5; ctx.stroke();\r\n\r\n  \/\/ Tra\u00een\u00e9e\r\n  const maxTr=parseInt(trailSlider.value);\r\n  if (maxTr>0) {\r\n    trail.forEach((p,i)=>{\r\n      const alpha=(i\/trail.length)*0.55;\r\n      const r=Math.max(0.5,3.5*(i\/trail.length));\r\n      ctx.beginPath(); ctx.arc(p[0],p[1],r,0,2*Math.PI);\r\n      ctx.fillStyle=`rgba(24,95,165,${alpha.toFixed(2)})`; ctx.fill();\r\n    });\r\n  }\r\n\r\n  \/\/ Point mobile\r\n  ctx.beginPath(); ctx.arc(pos[0],pos[1],7,0,2*Math.PI);\r\n  ctx.fillStyle=C.shape; ctx.fill();\r\n  ctx.beginPath(); ctx.arc(pos[0],pos[1],3.5,0,2*Math.PI);\r\n  ctx.fillStyle=C.bg; ctx.fill();\r\n\r\n  \/\/ Point de d\u00e9part\r\n  const [sx,sy]=posOnShape(shape,0);\r\n  ctx.beginPath(); ctx.arc(sx,sy,4.5,0,2*Math.PI);\r\n  ctx.fillStyle=C.accent; ctx.fill();\r\n\r\n  ctx.font='11px monospace'; ctx.fillStyle=C.text2; ctx.textAlign='left';\r\n  ctx.fillText(`t = ${t.toFixed(3)}`,10,CS-8);\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   RENDU 2 \u2014 POSITION \/ TEMPS\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction renderTime() {\r\n  const ctx=ctxT;\r\n  ctx.clearRect(0,0,CS,CS); ctx.fillStyle=C.bg; ctx.fillRect(0,0,CS,CS);\r\n  const PAD=42, W=CS-PAD*2, H=CS-PAD*2;\r\n\r\n  ctx.font='10px monospace'; ctx.fillStyle=C.text2;\r\n  for (let i=0;i<=4;i++) {\r\n    const y=PAD+H*i\/4;\r\n    ctx.beginPath(); ctx.moveTo(PAD,y); ctx.lineTo(PAD+W,y);\r\n    ctx.strokeStyle=C.grid; ctx.lineWidth=0.5; ctx.stroke();\r\n    ctx.textAlign='right'; ctx.fillText((1-i\/4).toFixed(2),PAD-5,y+3);\r\n  }\r\n  for (let i=0;i<=4;i++) {\r\n    const x=PAD+W*i\/4;\r\n    ctx.beginPath(); ctx.moveTo(x,PAD); ctx.lineTo(x,PAD+H);\r\n    ctx.strokeStyle=C.grid; ctx.lineWidth=0.5; ctx.stroke();\r\n    ctx.textAlign='center'; ctx.fillText((i\/4).toFixed(2),x,PAD+H+13);\r\n  }\r\n  ctx.strokeStyle=C.border; ctx.lineWidth=0.5; ctx.strokeRect(PAD,PAD,W,H);\r\n  ctx.fillStyle=C.text1; ctx.textAlign='center';\r\n  ctx.fillText('t (cycle)',PAD+W\/2,CS-4);\r\n\r\n  if (timeHistory.length>1) {\r\n    ['nx','ny'].forEach((key,ki)=>{\r\n      ctx.beginPath();\r\n      timeHistory.forEach((d,i)=>{\r\n        const px=PAD+d.t*W, py=PAD+H-d[key]*H;\r\n        i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);\r\n      });\r\n      ctx.strokeStyle=ki===0?C.shape:C.accent;\r\n      ctx.lineWidth=ki===0?1.8:1.2;\r\n      ctx.setLineDash(ki===0?[]:[4,3]); ctx.stroke(); ctx.setLineDash([]);\r\n    });\r\n    const last=timeHistory[timeHistory.length-1];\r\n    ctx.beginPath(); ctx.arc(PAD+last.t*W,PAD+H-last.nx*H,4,0,2*Math.PI);\r\n    ctx.fillStyle=C.shape; ctx.fill();\r\n  }\r\n\r\n  ctx.font='11px monospace'; ctx.textAlign='left';\r\n  ctx.fillStyle=C.shape;  ctx.fillText('\u2014 X norm.',PAD+4,PAD+13);\r\n  ctx.fillStyle=C.accent; ctx.fillText('-- Y norm.',PAD+4,PAD+26);\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   RENDU 3 \u2014 FFT\r\n   Panneau divis\u00e9 en deux zones verticales :\r\n   \u2022 Haut (60%) : spectre d'amplitude |X(k)| et |Y(k)| en barres\r\n   \u2022 Bas  (35%) : spectre de phase \u03c6X(k) en ligne pointill\u00e9e\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction renderFFT() {\r\n  const ctx=ctxF;\r\n  ctx.clearRect(0,0,CS,CS); ctx.fillStyle=C.bg; ctx.fillRect(0,0,CS,CS);\r\n\r\n  const PAD_L=50, PAD_R=14, PAD_T=18, PAD_B=34;\r\n  const W=CS-PAD_L-PAD_R;\r\n  const FULL_H=CS-PAD_T-PAD_B;\r\n\r\n  const AMP_H  = Math.floor(FULL_H*0.60);\r\n  const PH_H   = Math.floor(FULL_H*0.32);\r\n  const MID_GAP= FULL_H - AMP_H - PH_H;\r\n  const PH_TOP = PAD_T + AMP_H + MID_GAP;\r\n\r\n  const fft = fftCache;\r\n  const maxHarm = parseInt(fftNSlider.value);\r\n\r\n  \/\/ Attente\r\n  if (!fft || fft.fX.length < 2) {\r\n    ctx.font='12px monospace'; ctx.fillStyle=C.text2; ctx.textAlign='center';\r\n    ctx.fillText('En attente d\\'un cycle\u2026',CS\/2,CS\/2);\r\n    return;\r\n  }\r\n\r\n  const nBins = Math.min(maxHarm+1, fft.fX.length);\r\n\r\n  \/* \u2500\u2500 ZONE AMPLITUDE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n  \/\/ Cadre\r\n  ctx.strokeStyle=C.border; ctx.lineWidth=0.5;\r\n  ctx.strokeRect(PAD_L, PAD_T, W, AMP_H);\r\n\r\n  \/\/ Grille amplitude\r\n  for (let i=0;i<=4;i++) {\r\n    const y=PAD_T+AMP_H*i\/4;\r\n    ctx.beginPath(); ctx.moveTo(PAD_L,y); ctx.lineTo(PAD_L+W,y);\r\n    ctx.strokeStyle=C.grid; ctx.lineWidth=0.5; ctx.stroke();\r\n    ctx.font='9px monospace'; ctx.fillStyle=C.text2; ctx.textAlign='right';\r\n    ctx.fillText((1-i\/4).toFixed(1), PAD_L-4, y+3);\r\n  }\r\n\r\n  \/\/ Label axe Y amplitude\r\n  ctx.save(); ctx.translate(13,PAD_T+AMP_H\/2); ctx.rotate(-Math.PI\/2);\r\n  ctx.textAlign='center'; ctx.fillStyle=C.text1; ctx.font='9px monospace';\r\n  ctx.fillText('|Amplitude|',0,0); ctx.restore();\r\n\r\n  \/\/ Max amplitude pour normaliser (k>=1 seulement)\r\n  let maxAmp=1e-9;\r\n  for (let k=1;k<nBins;k++) {\r\n    if (fft.fX[k]) maxAmp=Math.max(maxAmp,fft.fX[k].amp);\r\n    if (fft.fY[k]) maxAmp=Math.max(maxAmp,fft.fY[k].amp);\r\n  }\r\n\r\n  const binsVisible = nBins-1; \/\/ on ignore k=0 (composante DC)\r\n  const gap   = W \/ binsVisible;\r\n  const barW  = Math.max(2, gap*0.36);\r\n\r\n  let domKx=1, domKy=1;\r\n\r\n  for (let i=0;i<binsVisible;i++) {\r\n    const k=i+1;\r\n    if (k>=fft.fX.length) break;\r\n    const cx=PAD_L+(i+0.5)*gap;\r\n\r\n    \/\/ Barre X\r\n    const hX=(fft.fX[k].amp\/maxAmp)*AMP_H;\r\n    ctx.globalAlpha=0.82;\r\n    ctx.fillStyle=C.shape;\r\n    ctx.fillRect(cx-barW-1, PAD_T+AMP_H-hX, barW, hX);\r\n\r\n    \/\/ Barre Y\r\n    const hY=(fft.fY[k].amp\/maxAmp)*AMP_H;\r\n    ctx.fillStyle=C.accent;\r\n    ctx.globalAlpha=0.70;\r\n    ctx.fillRect(cx+1, PAD_T+AMP_H-hY, barW, hY);\r\n    ctx.globalAlpha=1;\r\n\r\n    if (fft.fX[k].amp > fft.fX[domKx].amp) domKx=k;\r\n    if (fft.fY[k].amp > fft.fY[domKy].amp) domKy=k;\r\n  }\r\n\r\n  \/\/ Annotation fr\u00e9q. dominante X\r\n  if (domKx>0 && domKx<binsVisible+1) {\r\n    const cxDom=PAD_L+(domKx-0.5)*gap;\r\n    ctx.font='9px monospace'; ctx.fillStyle=C.shape; ctx.textAlign='center';\r\n    ctx.fillText(`k=${domKx}`,cxDom,PAD_T+10);\r\n  }\r\n\r\n  \/* \u2500\u2500 ZONE PHASE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\r\n  ctx.strokeStyle=C.border; ctx.lineWidth=0.5;\r\n  ctx.strokeRect(PAD_L, PH_TOP, W, PH_H);\r\n\r\n  \/\/ Grille phase : \u2212\u03c0 en bas, +\u03c0 en haut\r\n  for (let i=0;i<=4;i++) {\r\n    const y=PH_TOP+PH_H*i\/4;\r\n    ctx.beginPath(); ctx.moveTo(PAD_L,y); ctx.lineTo(PAD_L+W,y);\r\n    ctx.strokeStyle=C.grid; ctx.lineWidth=0.5; ctx.stroke();\r\n    const val=(1-i\/4)*2*Math.PI - Math.PI;\r\n    ctx.font='9px monospace'; ctx.fillStyle=C.text2; ctx.textAlign='right';\r\n    const label = i===0?'+\u03c0' : i===2?'0' : i===4?'\u2212\u03c0' : '';\r\n    if (label) ctx.fillText(label, PAD_L-4, y+3);\r\n  }\r\n\r\n  \/\/ Label axe Y phase\r\n  ctx.save(); ctx.translate(13,PH_TOP+PH_H\/2); ctx.rotate(-Math.PI\/2);\r\n  ctx.textAlign='center'; ctx.fillStyle=C.amber; ctx.font='9px monospace';\r\n  ctx.fillText('phase X (rad)',0,0); ctx.restore();\r\n\r\n  \/\/ Ligne z\u00e9ro (phase=0)\r\n  const zeroY=PH_TOP+PH_H\/2;\r\n  ctx.beginPath(); ctx.moveTo(PAD_L,zeroY); ctx.lineTo(PAD_L+W,zeroY);\r\n  ctx.strokeStyle='rgba(186,117,23,0.2)'; ctx.lineWidth=0.5; ctx.stroke();\r\n\r\n  \/\/ Courbe phase X\r\n  ctx.beginPath();\r\n  let first=true;\r\n  for (let i=0;i<binsVisible;i++) {\r\n    const k=i+1;\r\n    if (k>=fft.fX.length) break;\r\n    const cx=PAD_L+(i+0.5)*gap;\r\n    const phNorm=(fft.fX[k].phase+Math.PI)\/(2*Math.PI); \/\/ 0=\u2212\u03c0, 1=+\u03c0\r\n    const py=PH_TOP+PH_H*(1-phNorm);\r\n    first ? (ctx.moveTo(cx,py), first=false) : ctx.lineTo(cx,py);\r\n  }\r\n  ctx.strokeStyle=C.amber; ctx.lineWidth=1.4;\r\n  ctx.setLineDash([3,3]); ctx.stroke(); ctx.setLineDash([]);\r\n\r\n  \/\/ Marqueurs phase\r\n  for (let i=0;i<binsVisible;i++) {\r\n    const k=i+1;\r\n    if (k>=fft.fX.length) break;\r\n    const cx=PAD_L+(i+0.5)*gap;\r\n    const phNorm=(fft.fX[k].phase+Math.PI)\/(2*Math.PI);\r\n    const py=PH_TOP+PH_H*(1-phNorm);\r\n    ctx.beginPath(); ctx.arc(cx,py,2,0,2*Math.PI);\r\n    ctx.fillStyle=C.amber; ctx.fill();\r\n  }\r\n\r\n  \/\/ Axe X commun (harmoniques)\r\n  for (let i=0;i<binsVisible;i++) {\r\n    const k=i+1;\r\n    if (k>=fft.fX.length) break;\r\n    const cx=PAD_L+(i+0.5)*gap;\r\n    ctx.font='9px monospace'; ctx.fillStyle=C.text2; ctx.textAlign='center';\r\n    ctx.fillText(k, cx, CS-PAD_B+14);\r\n  }\r\n  ctx.font='10px monospace'; ctx.fillStyle=C.text1; ctx.textAlign='center';\r\n  ctx.fillText('harmonique k', PAD_L+W\/2, CS-4);\r\n\r\n  \/\/ Mise \u00e0 jour infos\r\n  if (domKx>=1 && fft.fX[domKx])\r\n    infoDomX.textContent=`k=${domKx}  |A|=${fft.fX[domKx].amp.toFixed(3)}`;\r\n  if (domKy>=1 && fft.fY[domKy])\r\n    infoDomY.textContent=`k=${domKy}  |A|=${fft.fY[domKy].amp.toFixed(3)}`;\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   BOUCLE PRINCIPALE\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nfunction loop(ts) {\r\n  if (!paused) {\r\n    const dt  = Math.min((ts-lastTS)\/1000, 0.05);\r\n    const spd = parseInt(speedSlider.value)*0.04;\r\n    t += spd*dt;\r\n\r\n    if (t >= 1) {\r\n      t -= 1; cycle++;\r\n      infoCycle.textContent = cycle;\r\n      fftCache = computeFFT(); \/\/ calcul sur cycle complet\r\n      timeHistory = [];\r\n    }\r\n\r\n    const shape = getShape();\r\n    const pos   = posOnShape(shape, t);\r\n\r\n    const maxTr = parseInt(trailSlider.value);\r\n    trailPoints.push([...pos]);\r\n    if (trailPoints.length > maxTr) trailPoints.shift();\r\n\r\n    const n = normPos(shape, pos);\r\n    timeHistory.push({ t, nx:n.nx, ny:n.ny });\r\n    if (timeHistory.length > 512) timeHistory.shift();\r\n\r\n    \/\/ Rafra\u00eechir la FFT en cours de cycle (~toutes les 25 frames)\r\n    fftTick++;\r\n    if (fftTick >= 25) {\r\n      fftTick = 0;\r\n      if (timeHistory.length >= 32) fftCache = computeFFT();\r\n    }\r\n\r\n    renderShape(shape, pos, trailPoints);\r\n    renderTime();\r\n    renderFFT();\r\n\r\n    infoT.textContent  = t.toFixed(3);\r\n    infoPx.textContent = Math.round(pos[0]);\r\n    infoPy.textContent = Math.round(pos[1]);\r\n  }\r\n  lastTS = ts;\r\n  requestAnimationFrame(loop);\r\n}\r\n\r\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n   \u00c9V\u00c9NEMENTS\r\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\r\nspeedSlider.oninput = ()=>{ speedVal.textContent = speedSlider.value; };\r\ntrailSlider.oninput = ()=>{ trailVal.textContent = trailSlider.value; };\r\nfftNSlider.oninput  = ()=>{ fftNVal.textContent  = fftNSlider.value; fftCache = computeFFT(); };\r\n\r\nfunction fullReset() {\r\n  t=0; trailPoints=[]; timeHistory=[]; cycle=1;\r\n  fftCache=null; fftTick=0;\r\n  infoCycle.textContent=1;\r\n  infoDomX.textContent='\u2014'; infoDomY.textContent='\u2014';\r\n}\r\n\r\nshapeSelect.onchange = fullReset;\r\nbtnReset.onclick     = fullReset;\r\nbtnPlayPause.onclick = ()=>{\r\n  paused=!paused;\r\n  btnPlayPause.textContent = paused?'Reprendre':'Pause';\r\n};\r\n\r\nrequestAnimationFrame(loop);\r\n<\/script>\r\n\r\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-11938","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/11938","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spgoo.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11938"}],"version-history":[{"count":1,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/11938\/revisions"}],"predecessor-version":[{"id":11939,"href":"https:\/\/spgoo.org\/index.php?rest_route=\/wp\/v2\/pages\/11938\/revisions\/11939"}],"wp:attachment":[{"href":"https:\/\/spgoo.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11938"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}