{"project":{"id":"1w92cEV","userId":"test@test.com","username":null,"userPicture":null,"name":"Double Pendulum Chaos","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!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\">\n  <title>Double Pendulum Chaos</title>\n  <link rel=\"stylesheet\" href=\"/css/styles.css\">\n  <script src=\"https://unpkg.com/lucide@latest/dist/umd/lucide.js\"></script>\n</head>\n<body>\n  <div class=\"app\">\n    <header class=\"header\">\n      <h1 class=\"title\">Double Pendulum Chaos</h1>\n      <p class=\"subtitle\">Drag either bob to set position &bull; release to watch chaos unfold</p>\n    </header>\n    <div class=\"canvas-wrap\">\n      <canvas id=\"canvas\"></canvas>\n    </div>\n    <footer class=\"controls\">\n      <button id=\"btn-clear\" class=\"btn btn--ghost\"><i data-lucide=\"trash-2\"></i>Clear Trails</button>\n      <button id=\"btn-pause\" class=\"btn btn--ghost\"><i data-lucide=\"pause\"></i>Pause</button>\n      <button id=\"btn-spawn\" class=\"btn btn--primary\"><i data-lucide=\"plus\"></i>Spawn 50</button>\n    </footer>\n  </div>\n  <script type=\"module\" src=\"/js/main.js\"></script>\n</body>\n</html>\n"},{"name":"README.md","content":"# Blnq Project\n"}],"folders":[{"folder":"css","files":[{"name":"styles.css","content":"*,\n*::before,\n*::after {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\n:root {\n  --bg: #0a0a0f;\n  --surface: #14141f;\n  --border: #2a2a3a;\n  --text: #e8e8f0;\n  --text-dim: #8888aa;\n  --accent: #7c5cfc;\n  --accent-hover: #9a7dff;\n  --danger: #ff4466;\n}\n\nhtml, body {\n  height: 100%;\n  background: var(--bg);\n  color: var(--text);\n  font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;\n  overflow: hidden;\n}\n\n.app {\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n}\n\n.header {\n  padding: 16px 24px 8px;\n  text-align: center;\n  z-index: 2;\n}\n\n.title {\n  font-size: 1.1rem;\n  font-weight: 600;\n  letter-spacing: 0.08em;\n  text-transform: uppercase;\n  color: var(--text);\n}\n\n.subtitle {\n  font-size: 0.72rem;\n  color: var(--text-dim);\n  margin-top: 2px;\n}\n\n.canvas-wrap {\n  flex: 1;\n  position: relative;\n  overflow: hidden;\n}\n\ncanvas {\n  display: block;\n  width: 100%;\n  height: 100%;\n  cursor: grab;\n}\n\ncanvas.dragging {\n  cursor: grabbing;\n}\n\n.controls {\n  display: flex;\n  justify-content: center;\n  gap: 10px;\n  padding: 12px 24px 16px;\n  z-index: 2;\n}\n\n.btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 16px;\n  border: none;\n  border-radius: 8px;\n  font-family: inherit;\n  font-size: 0.78rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: background 0.2s, color 0.2s;\n  white-space: nowrap;\n}\n\n.btn i,\n.btn svg {\n  width: 15px;\n  height: 15px;\n}\n\n.btn--primary {\n  background: var(--accent);\n  color: #fff;\n}\n\n.btn--primary:hover {\n  background: var(--accent-hover);\n}\n\n.btn--ghost {\n  background: var(--surface);\n  color: var(--text-dim);\n  border: 1px solid var(--border);\n}\n\n.btn--ghost:hover {\n  color: var(--text);\n  border-color: var(--text-dim);\n}\n\n.btn--ghost.active {\n  color: var(--danger);\n  border-color: var(--danger);\n}\n"}],"folders":[]},{"folder":"js","files":[{"name":"main.js","content":"// ─── Double Pendulum Chaos Toy ───────────────────────────────────────\n// Lagrangian mechanics · RK4 integration · Canvas trails\n\nconst canvas = document.getElementById('canvas');\nconst ctx = canvas.getContext('2d');\nconst btnClear = document.getElementById('btn-clear');\nconst btnPause = document.getElementById('btn-pause');\nconst btnSpawn = document.getElementById('btn-spawn');\nconst wrap = canvas.parentElement;\n\nlucide.createIcons();\n\n// ── Physics constants ──\nconst G = 9.81;       // gravity\nconst L1 = 1.0;       // length of upper arm\nconst L2 = 1.0;       // length of lower arm\nconst M1 = 1.0;       // mass of upper bob\nconst M2 = 1.0;       // mass of lower bob\nconst DT = 0.003;     // timestep\nconst STEPS = 4;      // sub-steps per frame\n\n// ── Sim state ──\nlet pendulums = [];\nlet paused = false;\nlet dragging = null;   // { pendulum, bob: 1|2 }\nlet lastTime = 0;\n\n// ── Trail config ──\nconst TRAIL_MAX = 1600;\n\n// ── Pendulum class ──\nclass Pendulum {\n  constructor(angle1, angle2, omega1, omega2, hueOffset) {\n    this.angle1 = angle1;\n    this.angle2 = angle2;\n    this.omega1 = omega1;\n    this.omega2 = omega2;\n    this.hueOffset = hueOffset;\n    this.trail = [];\n    this.age = 0;\n    this.alive = true;\n  }\n\n  // Derivatives for RK4\n  derivs(a1, o1, a2, o2) {\n    const dA = a1 - a2;\n    const sinD = Math.sin(dA);\n    const cosD = Math.cos(dA);\n    const m = M1 + M2;\n\n    const den1 = L1 * (m - M2 * cosD * cosD);\n    const den2 = L2 * (m - M2 * cosD * cosD);\n\n    const alpha1 = (\n      -G * m * Math.sin(a1)\n      - G * M2 * Math.sin(a1 - 2 * a2)\n      - 2 * sinD * M2 * (o2 * o2 * L2 + o1 * o1 * L1 * cosD)\n    ) / den1;\n\n    const alpha2 = (\n      2 * sinD * (\n        o1 * o1 * L1 * m\n        + G * m * Math.cos(a1)\n        + o2 * o2 * L2 * M2 * cosD\n      )\n    ) / den2;\n\n    return [o1, alpha1, o2, alpha2];\n  }\n\n  // RK4 step\n  step(dt) {\n    const { angle1: a1, omega1: o1, angle2: a2, omega2: o2 } = this;\n\n    const k1 = this.derivs(a1, o1, a2, o2);\n\n    const a1_2 = a1 + k1[0] * dt / 2;\n    const o1_2 = o1 + k1[1] * dt / 2;\n    const a2_2 = a2 + k1[2] * dt / 2;\n    const o2_2 = o2 + k1[3] * dt / 2;\n    const k2 = this.derivs(a1_2, o1_2, a2_2, o2_2);\n\n    const a1_3 = a1 + k2[0] * dt / 2;\n    const o1_3 = o1 + k2[1] * dt / 2;\n    const a2_3 = a2 + k2[2] * dt / 2;\n    const o2_3 = o2 + k2[3] * dt / 2;\n    const k3 = this.derivs(a1_3, o1_3, a2_3, o2_3);\n\n    const a1_4 = a1 + k3[0] * dt;\n    const o1_4 = o1 + k3[1] * dt;\n    const a2_4 = a2 + k3[2] * dt;\n    const o2_4 = o2 + k3[3] * dt;\n    const k4 = this.derivs(a1_4, o1_4, a2_4, o2_4);\n\n    this.angle1 += dt / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]);\n    this.omega1 += dt / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]);\n    this.angle2 += dt / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]);\n    this.omega2 += dt / 6 * (k1[3] + 2 * k2[3] + 2 * k3[3] + k4[3]);\n  }\n\n  // Get bob positions in canvas coords\n  positions(cx, cy, scale) {\n    const x1 = cx + L1 * Math.sin(this.angle1) * scale;\n    const y1 = cy + L1 * Math.cos(this.angle1) * scale;\n    const x2 = x1 + L2 * Math.sin(this.angle2) * scale;\n    const y2 = y1 + L2 * Math.cos(this.angle2) * scale;\n    return { x1, y1, x2, y2 };\n  }\n}\n\n// ── Create initial pendulum ──\nfunction spawn(hueOffset, a1, a2) {\n  const p = new Pendulum(\n    a1 ?? (Math.PI * 0.7 + (Math.random() - 0.5) * 0.3),\n    a2 ?? (Math.PI * 0.5 + (Math.random() - 0.5) * 0.4),\n    0, 0,\n    hueOffset\n  );\n  pendulums.push(p);\n  return p;\n}\n\n// ── Resize ──\nfunction resize() {\n  const dpr = window.devicePixelRatio || 1;\n  canvas.width = wrap.clientWidth * dpr;\n  canvas.height = wrap.clientHeight * dpr;\n}\nwindow.addEventListener('resize', resize);\nresize();\n\n// ── Clear trails ──\nbtnClear.addEventListener('click', () => {\n  for (const p of pendulums) p.trail = [];\n});\n\n// ── Pause ──\nbtnPause.addEventListener('click', () => {\n  paused = !paused;\n  btnPause.innerHTML = paused\n    ? '<i data-lucide=\"play\"></i>Play'\n    : '<i data-lucide=\"pause\"></i>Pause';\n  btnPause.classList.toggle('active', paused);\n  lucide.createIcons();\n});\n\n// ── Spawn 50 ──\nbtnSpawn.addEventListener('click', () => {\n  for (let i = 0; i < 50; i++) {\n    spawn(\n      i * (360 / 50),\n      Math.PI * 0.7 + (Math.random() - 0.5) * 0.08,\n      Math.PI * 0.5 + (Math.random() - 0.5) * 0.08\n    );\n  }\n});\n\n// ── Drag interaction ──\nfunction getCanvasXY(e) {\n  const rect = canvas.getBoundingClientRect();\n  const touch = e.touches ? e.touches[0] : e;\n  return {\n    cx: (touch.clientX - rect.left) / rect.width * canvas.width,\n    cy: (touch.clientY - rect.top) / rect.height * canvas.height,\n    mx: touch.clientX,\n    my: touch.clientY,\n  };\n}\n\nfunction scale() {\n  return Math.min(canvas.width, canvas.height) * 0.28;\n}\n\nfunction pivot() {\n  return { x: canvas.width / 2, y: canvas.height * 0.38 };\n}\n\nfunction hitTest(cx, cy) {\n  const s = scale();\n  const piv = pivot();\n  for (let i = pendulums.length - 1; i >= 0; i--) {\n    const pos = pendulums[i].positions(piv.x, piv.y, s);\n    const r = 20;\n    if (Math.hypot(cx - pos.x2, cy - pos.y2) < r) {\n      return { pendulum: pendulums[i], bob: 2 };\n    }\n    if (Math.hypot(cx - pos.x1, cy - pos.y1) < r) {\n      return { pendulum: pendulums[i], bob: 1 };\n    }\n  }\n  return null;\n}\n\nfunction onDown(e) {\n  const { cx, cy } = getCanvasXY(e);\n  const hit = hitTest(cx, cy);\n  if (hit) {\n    dragging = hit;\n    hit.pendulum.omega1 = 0;\n    hit.pendulum.omega2 = 0;\n    canvas.classList.add('dragging');\n    e.preventDefault();\n  }\n}\n\nfunction onMove(e) {\n  if (!dragging) return;\n  const { cx, cy } = getCanvasXY(e);\n  const s = scale();\n  const piv = pivot();\n  const p = dragging.pendulum;\n\n  if (dragging.bob === 2) {\n    // Calculate angle2 from bob1 → cursor\n    const pos1 = p.positions(piv.x, piv.y, s);\n    p.angle2 = Math.atan2(cx - pos1.x1, cy - pos1.y1);\n  }\n  // Calculate angle1 from pivot → cursor (or bob1)\n  p.angle1 = Math.atan2(cx - piv.x, cy - piv.y);\n  // If dragging bob2, also adjust angle2 relative to new bob1\n  if (dragging.bob === 2) {\n    const pos1 = p.positions(piv.x, piv.y, s);\n    const x2 = cx;\n    const y2 = cy;\n    p.angle2 = Math.atan2(x2 - pos1.x1, y2 - pos1.y1);\n  }\n  e.preventDefault();\n}\n\nfunction onUp() {\n  if (dragging) {\n    dragging.pendulum.omega1 = 0;\n    dragging.pendulum.omega2 = 0;\n    dragging = null;\n    canvas.classList.remove('dragging');\n  }\n}\n\ncanvas.addEventListener('mousedown', onDown);\ncanvas.addEventListener('mousemove', onMove);\ncanvas.addEventListener('mouseup', onUp);\ncanvas.addEventListener('mouseleave', onUp);\ncanvas.addEventListener('touchstart', onDown, { passive: false });\ncanvas.addEventListener('touchmove', onMove, { passive: false });\ncanvas.addEventListener('touchend', onUp);\n\n// ── Render ──\nfunction render(time) {\n  requestAnimationFrame(render);\n\n  // Physics\n  if (!paused) {\n    for (const p of pendulums) {\n      if (p === dragging?.pendulum) continue;\n      for (let i = 0; i < STEPS; i++) {\n        p.step(DT);\n      }\n      p.age += DT * STEPS;\n    }\n  }\n\n  // Clear to near-black\n  const W = canvas.width;\n  const H = canvas.height;\n  ctx.fillStyle = '#0a0a0f';\n  ctx.fillRect(0, 0, W, H);\n\n  // Draw subtle center line\n  const piv = pivot();\n  ctx.strokeStyle = 'rgba(255,255,255,0.03)';\n  ctx.lineWidth = 1;\n  ctx.beginPath();\n  ctx.moveTo(piv.x, 0);\n  ctx.lineTo(piv.x, H);\n  ctx.stroke();\n\n  const s = scale();\n  const hueBase = (time * 0.01) % 360;\n\n  // ── Draw trails ──\n  for (const p of pendulums) {\n    const trail = p.trail;\n    if (trail.length < 2) continue;\n\n    for (let i = 1; i < trail.length; i++) {\n      const alpha = (i / trail.length) * 0.85;\n      const hue = (p.hueOffset + i * 0.3) % 360;\n      ctx.strokeStyle = `hsla(${hue}, 85%, 60%, ${alpha})`;\n      ctx.lineWidth = 1.8;\n      ctx.beginPath();\n      ctx.moveTo(trail[i - 1].x, trail[i - 1].y);\n      ctx.lineTo(trail[i].x, trail[i].y);\n      ctx.stroke();\n    }\n  }\n\n  // ── Draw pendulums ──\n  for (let idx = 0; idx < pendulums.length; idx++) {\n    const p = pendulums[idx];\n    const pos = p.positions(piv.x, piv.y, s);\n\n    // Record trail\n    if (!paused && p !== dragging?.pendulum) {\n      p.trail.push({ x: pos.x2, y: pos.y2 });\n      if (p.trail.length > TRAIL_MAX) p.trail.shift();\n    }\n\n    const hue = (hueBase + p.hueOffset) % 360;\n    const isDragging = p === dragging?.pendulum;\n\n    // Arms\n    ctx.strokeStyle = isDragging ? 'rgba(255,255,255,0.9)' : `hsla(${hue}, 50%, 70%, 0.7)`;\n    ctx.lineWidth = isDragging ? 2.5 : 1.8;\n    ctx.beginPath();\n    ctx.moveTo(piv.x, piv.y);\n    ctx.lineTo(pos.x1, pos.y1);\n    ctx.lineTo(pos.x2, pos.y2);\n    ctx.stroke();\n\n    // Joints\n    ctx.fillStyle = `hsla(${hue}, 70%, 80%, 0.9)`;\n    ctx.beginPath();\n    ctx.arc(pos.x1, pos.y1, 5, 0, Math.PI * 2);\n    ctx.fill();\n\n    // Bob\n    const bobR = isDragging ? 12 : 9;\n    const grad = ctx.createRadialGradient(pos.x2, pos.y2, 0, pos.x2, pos.y2, bobR * 2);\n    grad.addColorStop(0, `hsla(${hue}, 90%, 70%, 1)`);\n    grad.addColorStop(0.5, `hsla(${hue}, 85%, 55%, 0.8)`);\n    grad.addColorStop(1, `hsla(${hue}, 80%, 40%, 0)`);\n    ctx.fillStyle = grad;\n    ctx.beginPath();\n    ctx.arc(pos.x2, pos.y2, bobR * 2, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.fillStyle = `hsla(${hue}, 90%, 75%, 1)`;\n    ctx.beginPath();\n    ctx.arc(pos.x2, pos.y2, bobR * 0.6, 0, Math.PI * 2);\n    ctx.fill();\n  }\n\n  // Pivot\n  ctx.fillStyle = '#ffffff';\n  ctx.beginPath();\n  ctx.arc(piv.x, piv.y, 4, 0, Math.PI * 2);\n  ctx.fill();\n\n  // Counter\n  ctx.fillStyle = 'rgba(255,255,255,0.25)';\n  ctx.font = `${Math.round(W * 0.012)}px monospace`;\n  ctx.textAlign = 'left';\n  ctx.fillText(`${pendulums.length} pendulum${pendulums.length !== 1 ? 's' : ''}`, 16, H - 16);\n}\n\n// ── Initial pendulums ──\nspawn(0, Math.PI * 0.75, Math.PI * 0.5);\nspawn(120, Math.PI * 0.8, Math.PI * 0.45);\nspawn(240, Math.PI * 0.7, Math.PI * 0.55);\n\nrequestAnimationFrame(render);\n"}],"folders":[]},{"folder":"other","files":[{"name":"main.sketch","content":""}],"folders":[],"collapsed":true}]},"variants":null,"createdAt":"2026-09-03T13:29:37.741Z","updatedAt":"2026-09-03T13:29:44.607Z","hasThumbnail":true}}