0

I am working on modifying an example of warp.js to allow for warping using a grid of control point dots (like the AI "free distort" tool)

I have had tried myself, I have abused ChatGPT and even paid a guy on Fiverr but no one and no bot has been able to help.

const svg = document.getElementById('svg8');
const controlPath = document.getElementById('control-path');

const warp = new Warp(svg);
const width = svg.width.baseVal.value;
const height = svg.height.baseVal.value;


// Define the number of columns and rows in the grid
const numColumns = 5;
const numRows = 5;

// Calculate the grid cell size
const cellWidth = width / (numColumns - 1);
const cellHeight = height / (numRows - 1);

// Create the initial control points grid
const controlPoints = [];
for (let row = 0; row < numRows; row++) {
  for (let col = 0; col < numColumns; col++) {
    const x = col * cellWidth;
    const y = row * cellHeight;
    controlPoints.push([x, y]);
  }
}

// Funny things happen when control points are positioned perfectly on other points... buff it out
const controlBuffer = 0.1;
for (let i = 0; i < controlPoints.length; i++) {
  const point = controlPoints[i];
  if (point[0] === 0) point[0] -= controlBuffer;
  if (point[1] === 0) point[1] -= controlBuffer;
  if (point[0] === width) point[0] += controlBuffer;
  if (point[1] === height) point[1] += controlBuffer;
}

// Compute weights from control points
warp.transform(function(v0, V = controlPoints) {
  const A = [];
  const W = [];
  const L = [];

  // Find distances
  for (let i = 0; i < V.length; i++) {
    const j = (i + 1) % V.length;

    const vi = V[i];
    const vj = V[j];

    const dx = vj[0] - vi[0];
    const dy = vj[1] - vi[1];

    const r = Math.sqrt(dx ** 2 + dy ** 2);

    W[i] = r;
  }

  // Find angles
  for (let i = 0; i < V.length; i++) {
    const j = (i + 1) % V.length;

    const vi = V[i];
    const vj = V[j];

    const dx = vj[0] - v0[0];
    const dy = vj[1] - v0[1];

    const r = Math.sqrt(dx ** 2 + dy ** 2);
    const angle = Math.atan2(dy, dx);

    A[i] = angle;
    W[i] = r;
  }

  // Normalize weights
  const Ws = W.reduce((a, b) => a + b, 0);
  for (let i = 0; i < V.length; i++) {
    L[i] = W[i] / Ws;
  }

  // Save weights to the point for use when transforming
  return [...v0, ...L];
});

// Function to reposition the control points based on the mean value coordinates
function reposition([x, y, ...W], V = controlPoints) {
  let nx = 0;
  let ny = 0;
  let s = 0;

  // Recreate the points using mean value coordinates
  for (let i = 0; i < V.length; i++) {
    const weight = W[i];
    const point = V[i];

    nx += weight * point[0];
    ny += weight * point[1];
    s += weight;
  }

  nx /= s;
  ny /= s;

  return [nx, ny, ...W];
}

// Function to draw the control shape
function drawControlShape(element = controlPath, V = controlPoints) {
  const path = [];

  for (let row = 0; row < numRows; row++) {
    const rowPoints = V.slice(row * numColumns, (row + 1) * numColumns);
    path.push(`M${rowPoints[0][0]} ${rowPoints[0][1]}`);

    for (let col = 1; col < numColumns; col++) {
      path.push(`L${rowPoints[col][0]} ${rowPoints[col][1]}`);
    }
  }

  for (let col = 0; col < numColumns; col++) {
    const colPoints = [];
    for (let row = 0; row < numRows; row++) {
      colPoints.push(V[row * numColumns + col]);
    }
    path.push(`M${colPoints[0][0]} ${colPoints[0][1]}`);

    for (let row = 1; row < numRows; row++) {
      path.push(`L${colPoints[row][0]} ${colPoints[row][1]}`);
    }
  }

  element.setAttribute('d', path.join(''));
}

// Add code to draw dots at each control point
const svgControl = document.getElementById('svg-control');
for (let i = 0; i < controlPoints.length; i++) {
  const [x, y] = controlPoints[i];
  const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  dot.setAttribute('cx', x);
  dot.setAttribute('cy', y);
  dot.setAttribute('r', 5);
  dot.setAttribute('fill', 'red');
  svgControl.appendChild(dot);
}

warp.interpolate(40);
drawControlShape();
//warp.transform(reposition);

// Get the SVG circle elements
var dots = document.querySelectorAll('#svg-control circle');

// Function to initiate dragging
function startDrag(event) {
  // Store the current dot and its initial position
  const activeDot = event.target;
  let initialX = event.clientX;
  let initialY = event.clientY;
  let offsetX = parseInt(activeDot.getAttribute('cx'));
  let offsetY = parseInt(activeDot.getAttribute('cy'));

  // Get the index of the active dot in the dots NodeList
  const index = Array.from(dots).indexOf(activeDot);

  // Add event listeners for mouse move and mouse up events
  document.addEventListener('mousemove', drag);
  document.addEventListener('mouseup', endDrag);

  // Function to handle dragging
  function drag(event) {
    // Calculate the distance moved by the mouse
    const deltaX = event.clientX - initialX;
    const deltaY = event.clientY - initialY;

    // Update the position of the dot based on the mouse movement
    const newX = offsetX + deltaX;
    const newY = offsetY + deltaY;
    activeDot.setAttribute('cx', newX);
    activeDot.setAttribute('cy', newY);

    // Update the controlPoints array with the new positions
    controlPoints[index] = [newX, newY];

    // Redraw the control shape
    drawControlShape();

    // Update the warp transformation based on the new control points
    warp.transform(reposition);
  }

  // Function to end dragging
  function endDrag() {
    // Remove the event listeners for mouse move and mouse up events
    document.removeEventListener('mousemove', drag);
    document.removeEventListener('mouseup', endDrag);
  }
}

// Add event listeners for mouse interactions on each dot
dots.forEach(dot => {
  dot.addEventListener('mousedown', startDrag);
});
svg {
  position: absolute;
  top: 0px;
  left: 0px;
}

#svg8,
#svg-control {
  position: absolute;
  top: 0px;
  left: 0px;
  overflow: visible;
}

#svg-control {
  position: absolute;
  z-index: 9000000;
  top: 0;
  left: 0;
}

#control-path {
  fill: none;
  stroke: red;
  stroke-width: 1px;
}

body {
  padding: 10px;
}
<script src="https://cdn.jsdelivr.net/npm/warpjs@1.0.8/dist/warp.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 105" height="500px" width="500px" id="svg8">
    
  <g fill="#97C024" stroke="#97C024" stroke-linejoin="round" stroke-linecap="round">
    <path d="M14,40v24M81,40v24M38,68v24M57,68v24M28,42v31h39v-31z" stroke-width="12"/>
    <path d="M32,5l5,10M64,5l-6,10 " stroke-width="2"/>
  </g>
  <path d="M22,35h51v10h-51zM22,33c0-31,51-31,51,0" fill="#97C024"/>
  <g fill="#FFF">
    <circle cx="36" cy="22" r="2"/>
    <circle cx="59" cy="22" r="2"/>
  </g>
</svg>

<svg id="svg-control" width="120" height="120">
     <path id="control-path" d="M50 50L100 50L100 100L50 100Z" stroke="black" fill="transparent"/>
  </svg>

Here is a jsfiddle I have made for it: https://jsfiddle.net/b70mdz5L/

The bug is it warps to almost nothing depending on the svg element from just touching a control point.

Here is the original example that I simplified(had it working) and modified. https://pavellaptev.github.io/warp-svg/

I'd like to think I exhausted all of my option before just spamming the forum. So if anyone could help I would really appreciated it.

  • Sorry, I think a **hull distortion** is the best you can get with warp.js (after some frustrating testing - especially considering the ... not so great documentation). However, you can submit a [Feature Request] (https://github.com/benjamminf/warpjs/issues). In fact your example codes distorts the image (inspect it in Dev tools) – but not as expected, because the inner mesh nodes will result in pretty chaotic point calculations. – herrstrietzel Jul 20 '23 at 00:45

0 Answers0