Rotating Vector Field — D3 + GSAP ScrollTrigger
``` Notes: - Adjust cols/rows, arrow length, or the ScrollTrigger end distance to change density and how much rotation occurs during the pinned scroll. - Change Math.PI * 4 to control total rotation (e.g., Math.PI * 2 for one full turn). -->
        
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Rotating Vector Field — D3 + GSAP ScrollTrigger</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/gsap.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/ScrollTrigger.min.js"></script>
  <style>
    body { margin: 0; font-family: sans-serif; }
    /* Tall content so page can scroll */
    #body {
      height: 800vh;
      position: relative;
    }
    #canvas-wrap {
      position: sticky;
      top: 0;
      height: 100vh;
      width: 100vw;
      background: #0b1220;
    }
    svg { display: block; width: 100%; height: 100%; }
    .arrow { stroke-width: 1.5px; fill: none; stroke-linecap: round; }
  </style>
</head>
<body>

    <div id="body">
      <div id="canvas-wrap">
      </div>
    </div>
  <!-- SVG will be appended here -->

<script>
/* --- Setup D3 canvas and arrows --- */
const container = document.getElementById('canvas-wrap');
const width = container.clientWidth;
const height = container.clientHeight;
const svg = d3.select(container)
  .append('svg')
  .attr('viewBox', `0 0 ${width} ${height}`)
  .attr('preserveAspectRatio', 'xMidYMid slice');

const cols = 18, rows = 12;
const padding = 60;
const xScale = d3.scaleLinear().domain([0, cols-1]).range([padding, width - padding]);
const yScale = d3.scaleLinear().domain([0, rows-1]).range([padding, height - padding]);

function arrowPath(x, y, angle, len = 28, head = 8) {
  const dx = Math.cos(angle) * len, dy = Math.sin(angle) * len;
  const leftAngle = angle + Math.PI * 2/3;
  const rightAngle = angle - Math.PI * 2/3;
  const lx = x + dx - Math.cos(leftAngle) * head;
  const ly = y + dy - Math.sin(leftAngle) * head;
  const rx = x + dx - Math.cos(rightAngle) * head;
  const ry = y + dy - Math.sin(rightAngle) * head;
  return `M${x},${y} L${x+dx},${y+dy} M${lx},${ly} L${x+dx},${y+dy} L${rx},${ry}`;
}

const points = [];
for (let j = 0; j < rows; j++) {
  for (let i = 0; i < cols; i++) {
    points.push({i, j, x: xScale(i), y: yScale(j)});
  }
}

const arrows = svg.selectAll('path.arrow')
  .data(points)
  .enter()
  .append('path')
  .attr('class', 'arrow')
  .attr('stroke', d => d3.interpolateTurbo((d.i/cols + d.j/rows)/2))
  .attr('d', d => arrowPath(d.x, d.y, 0));

// Field angle computation uses a global rotation variable we will tween with GSAP.
let globalRotation = 0;
function fieldAngle(xIdx, yIdx, tGlobal) {
  const cx = (cols-1)/2, cy = (rows-1)/2;
  const dx = xIdx - cx, dy = yIdx - cy;
  const base = Math.atan2(dy, dx);
  const swirl = Math.sin(Math.hypot(dx,dy) / 2) * 0.8;
  return base + swirl + tGlobal;
}

/* --- Render loop (keeps arrows updated) --- */
let last = performance.now();
function renderLoop(now) {
  const dt = (now - last) / 1000;
  last = now;
  // update arrows using current globalRotation
  arrows.attr('d', d => arrowPath(d.x, d.y, fieldAngle(d.i, d.j, globalRotation), 28, 8));
  requestAnimationFrame(renderLoop);
}
requestAnimationFrame(renderLoop);

/* --- GSAP ScrollTrigger: pin canvas and control rotation --- */
gsap.registerPlugin(ScrollTrigger);

// We will animate globalRotation from 0 -> 4*PI (two full turns) as user scrolls the pinned section.
const tweenObj = { rotation: 0 };
gsap.to(tweenObj, {
  rotation: Math.PI * 4,               // total rotation over the scroll
  ease: 'none',
  onUpdate: () => { globalRotation = tweenObj.rotation; },
  scrollTrigger: {
    trigger: '#body',
    start: 'top top',
    end: 'bottom bottom',                     // scroll distance while pinned (adjust as desired)
    scrub: 0.8,                        // smooth scrub
    pin: true,
    anticipatePin: 1,
  }
});
</script>

</body>
</html>

<!--

This conversation was generated with Duck.ai (https://duck.ai) using OpenAI's GPT-5 mini Model. AI chats may display inaccurate or offensive information (see https://duckduckgo.com/duckai/privacy-terms for more info).

====================

User prompt 1 of 2 - 2/16/2026, 7:06:01 PM:
Render me a simple vector field turning around in d3js

GPT-5 mini:
## Simple rotating vector field in D3.js

Below is a minimal HTML + JavaScript example that draws a 2D vector field where each arrow rotates (turns around) over time. Copy into an .html file and open in a browser.

```html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Rotating Vector Field — D3</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <style>
    body { margin: 0; }
    svg { display: block; width: 100vw; height: 100vh; background: #111; }
    .arrow { stroke: #ffd; fill: none; stroke-width: 1.5px; }
    .shaft { stroke-linecap: round; }
  </style>
</head>
<body>
<script>
const width = window.innerWidth, height = window.innerHeight;
const svg = d3.select("body")
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`);

const cols = 16, rows = 12;
const padding = 60;
const xScale = d3.scaleLinear().domain([0, cols-1]).range([padding, width - padding]);
const yScale = d3.scaleLinear().domain([0, rows-1]).range([padding, height - padding]);

// Arrow drawing helper using a small path (shaft + head)
function arrowPath(x, y, angle, len = 28, head = 8) {
  const dx = Math.cos(angle) * len, dy = Math.sin(angle) * len;
  const hx = Math.cos(angle) * head, hy = Math.sin(angle) * head;
  // Shaft from (x,y) to (x+dx,y+dy)
  // Head is two short lines at ±120° from the main direction
  const leftAngle = angle + Math.PI * 2/3;
  const rightAngle = angle - Math.PI * 2/3;
  const lx = x + dx - Math.cos(leftAngle) * head;
  const ly = y + dy - Math.sin(leftAngle) * head;
  const rx = x + dx - Math.cos(rightAngle) * head;
  const ry = y + dy - Math.sin(rightAngle) * head;
  return `M${x},${y} L${x+dx},${y+dy} M${lx},${ly} L${x+dx},${y+dy} L${rx},${ry}`;
}

// Create grid points
const points = [];
for (let j = 0; j < rows; j++) {
  for (let i = 0; i < cols; i++) {
    points.push({i, j, x: xScale(i), y: yScale(j)});
  }
}

const arrows = svg.selectAll("path.arrow")
  .data(points)
  .enter()
  .append("path")
  .attr("class", "arrow")
  .attr("stroke", d => d3.interpolateTurbo((d.i/cols + d.j/rows)/2))
  .attr("d", d => arrowPath(d.x, d.y, 0));

// Vector field function: base direction depends on position; add global rotation over time
function fieldAngle(xIdx, yIdx, t) {
  // base swirl: angle around center
  const cx = (cols-1)/2, cy = (rows-1)/2;
  const dx = xIdx - cx, dy = yIdx - cy;
  const base = Math.atan2(dy, dx);          // radial angle
  const swirl = Math.sin(Math.hypot(dx,dy) / 2) * 0.8; // position-dependent twist
  const global = t * 0.8;                   // global rotation speed
  return base + swirl + global;
}

// Animation loop
let start = Date.now();
d3.timer(() => {
  const t = (Date.now() - start) / 1000; // seconds
  arrows.attr("d", d => arrowPath(d.x, d.y, fieldAngle(d.i, d.j, t), 28, 8));
});
</script>
</body>
</html>
```

Usage notes:
- Adjust `cols`, `rows`, `len`, or `global` speed to change density, arrow length, or rotation speed.
- Replace color/styling as desired.

--------------------

User prompt 2 of 2 - 2/16/2026, 7:06:53 PM:
Now add gsap scroll trigger to it so when I scroll the website pinned canvas rotate vectors

GPT-5 mini:
## Rotating vector field with GSAP ScrollTrigger (pinned canvas on scroll)

Copy this into an .html file. It uses D3 to draw the vector field and GSAP + ScrollTrigger to pin the SVG canvas and animate the global rotation angle as you scroll.

```html
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Rotating Vector Field — D3 + GSAP ScrollTrigger</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/gsap.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/ScrollTrigger.min.js"></script>
  <style>
    body { margin: 0; font-family: sans-serif; }
    /* Tall content so page can scroll */
    .spacer { height: 120vh; background: linear-gradient(#222, #111); }
    #canvas-wrap {
      position: relative;
      height: 100vh;
      overflow: hidden;
      background: #0b1220;
    }
    svg { display: block; width: 100%; height: 100%; }
    .arrow { stroke-width: 1.5px; fill: none; stroke-linecap: round; }
  </style>
</head>
<body>

<div class="spacer"></div>

<div id="canvas-wrap">
  <!-- SVG will be appended here -->
</div>

<div class="spacer"></div>

<script>
/* --- Setup D3 canvas and arrows --- */
const container = document.getElementById('canvas-wrap');
const width = container.clientWidth;
const height = container.clientHeight;
const svg = d3.select(container)
  .append('svg')
  .attr('viewBox', `0 0 ${width} ${height}`)
  .attr('preserveAspectRatio', 'xMidYMid slice');

const cols = 18, rows = 12;
const padding = 60;
const xScale = d3.scaleLinear().domain([0, cols-1]).range([padding, width - padding]);
const yScale = d3.scaleLinear().domain([0, rows-1]).range([padding, height - padding]);

function arrowPath(x, y, angle, len = 28, head = 8) {
  const dx = Math.cos(angle) * len, dy = Math.sin(angle) * len;
  const leftAngle = angle + Math.PI * 2/3;
  const rightAngle = angle - Math.PI * 2/3;
  const lx = x + dx - Math.cos(leftAngle) * head;
  const ly = y + dy - Math.sin(leftAngle) * head;
  const rx = x + dx - Math.cos(rightAngle) * head;
  const ry = y + dy - Math.sin(rightAngle) * head;
  return `M${x},${y} L${x+dx},${y+dy} M${lx},${ly} L${x+dx},${y+dy} L${rx},${ry}`;
}

const points = [];
for (let j = 0; j < rows; j++) {
  for (let i = 0; i < cols; i++) {
    points.push({i, j, x: xScale(i), y: yScale(j)});
  }
}

const arrows = svg.selectAll('path.arrow')
  .data(points)
  .enter()
  .append('path')
  .attr('class', 'arrow')
  .attr('stroke', d => d3.interpolateTurbo((d.i/cols + d.j/rows)/2))
  .attr('d', d => arrowPath(d.x, d.y, 0));

// Field angle computation uses a global rotation variable we will tween with GSAP.
let globalRotation = 0;
function fieldAngle(xIdx, yIdx, tGlobal) {
  const cx = (cols-1)/2, cy = (rows-1)/2;
  const dx = xIdx - cx, dy = yIdx - cy;
  const base = Math.atan2(dy, dx);
  const swirl = Math.sin(Math.hypot(dx,dy) / 2) * 0.8;
  return base + swirl + tGlobal;
}

/* --- Render loop (keeps arrows updated) --- */
let last = performance.now();
function renderLoop(now) {
  const dt = (now - last) / 1000;
  last = now;
  // update arrows using current globalRotation
  arrows.attr('d', d => arrowPath(d.x, d.y, fieldAngle(d.i, d.j, globalRotation), 28, 8));
  requestAnimationFrame(renderLoop);
}
requestAnimationFrame(renderLoop);

/* --- GSAP ScrollTrigger: pin canvas and control rotation --- */
gsap.registerPlugin(ScrollTrigger);

// We will animate globalRotation from 0 -> 4*PI (two full turns) as user scrolls the pinned section.
const tweenObj = { rotation: 0 };
gsap.to(tweenObj, {
  rotation: Math.PI * 4,               // total rotation over the scroll
  ease: 'none',
  onUpdate: () => { globalRotation = tweenObj.rotation; },
  scrollTrigger: {
    trigger: '#canvas-wrap',
    start: 'top top',
    end: '+=1500',                     // scroll distance while pinned (adjust as desired)
    scrub: 0.8,                        // smooth scrub
    pin: true,
    anticipatePin: 1,
  }
});
</script>

</body>
</html>
```

Notes:
- Adjust cols/rows, arrow length, or the ScrollTrigger end distance to change density and how much rotation occurs during the pinned scroll.
- Change Math.PI * 4 to control total rotation (e.g., Math.PI * 2 for one full turn).

-->