Question about TexttoPoint()

Hi, I have a question about TexttoPoint() in p5.js.

Is there a way to set/limit the number of points that draw the letter?

For example, I’d like to draw both letters ‘A’ and ‘B’ only with 10 points but not sure how… (Using the option sampleFactor creates points based on path-length so ‘A’ and ‘B’ would different number of points). Or would there be a way to do this using something other than TexttoPoint()?

I’d really appreciate any help!

https://p5js.org/reference/#/p5.Font/textToPoints

Hi, thanks for the reply but these options don’t achieve what I asked, or am I not understanding correctly? I’m new to programming so I’d appreciate if you could clarify?

pts = caslon.textToPoints('NYTimes', 0, 0, 120,{
    sampleFactor: 0.1, //the higher the number, the more detail
    simplifyThreshold: 0
  });

code reference (unmodified code)
https://editor.p5js.org/aferriss/sketches/B1BOfBdZX

Yes, I get that but ‘N’ and ‘Y’ from ‘NYTimes’ will still have different number of points, because sampleFactor changes the distance between the points rather than the number of points.

For example, I have ‘0’ and ‘4’ but they are drawn with different number of ellipses. How can I make sure both have the same number of ellipses? Please let me know if my question is unclear…

Hi,

You have two possible solutions :

  • Take an arbitrary large number for the sampleFactor parameter like 100 for both letters and take the quantity you want out of those arrays :

    // The textToPoints options
    const options = {
      sampleFactor: 100,
      simplifyThreshold: 0
    }
    
    // The target number of points
    const nPoints = 20;
    
    // Use slice to get nPoints first elements of the array
    const zero = font.textToPoints("0", 0, 0, 10, options).slice(0, nPoints);
    const four = font.textToPoints("4", 0, 0, 10, options).slice(0, nPoints);
    
    // Display your points...
    

    I didn’t test it but it should work. The issue is that you need to compute a lot of points to maybe take 1/10 of it so it’s computationally ineffective.

  • Otherwise look at the source code and try to implement your own function :wink: :

p5.Font.prototype.textToPoints = function(txt, x, y, fontSize, options) {
  let xoff = 0;
  const result = [];
  const glyphs = this._getGlyphs(txt);

  function isSpace(i) {
    return (
      (glyphs[i].name && glyphs[i].name === 'space') ||
      (txt.length === glyphs.length && txt[i] === ' ') ||
      (glyphs[i].index && glyphs[i].index === 3)
    );
  }

  fontSize = fontSize || this.parent._renderer._textSize;

  for (let i = 0; i < glyphs.length; i++) {
    if (!isSpace(i)) {
      // fix to #1817, #2069

      const gpath = glyphs[i].getPath(x, y, fontSize),
        paths = splitPaths(gpath.commands);

      for (let j = 0; j < paths.length; j++) {
        const pts = pathToPoints(paths[j], options);

        for (let k = 0; k < pts.length; k++) {
          pts[k].x += xoff;
          result.push(pts[k]);
        }
      }
    }

    xoff += glyphs[i].advanceWidth * this._scale(fontSize);
  }

  return result;
};

Hi, Thank you for the reply! I tried the first code but nothing was showing up. Could it be that I have integrated it incorrectly to my code?

let grotesk;
let fontSize = 400;

let zeroArray;
let fourArray;

function preload() {
  grotesk = loadFont('grotesk.otf');
}

function setup() {
  createCanvas(windowWidth,windowHeight);
  textFont(grotesk);
	
	const options = {
  	sampleFactor: 100,
  	simplifyThreshold: 0
	}
	const nPoints = 20;
	
	zeroArray = grotesk.textToPoints("0",100,300,options).slice(0, nPoints);
	fourArray = grotesk.textToPoints("4",500,300,options).slice(0, nPoints);
}

function draw() {
	background(220);
  textSize (fontSize);
	noStroke();
	
	zero();
	
}

function zero() {
	
	for (let i = 0; i < zeroArray.length; i++){
		fill (random(255));
		ellipse(zeroArray[i].x,zeroArray[i].y,5,5);		
		}
	
	for (let i = 0; i < fourArray.length; i++){
		ellipse(fourArray[i].x,fourArray[i].y,5,5);
		fill (random(20));
		}

}

Ok my bad, this is not the right solution but few things first :

  • Be careful, you forgot the fontSize parameter in textToPoints(txt, x, y, fontSize, [options]) so it wouldn’t work anyway.

  • Calling textSize(fontSize); in the draw() function doesn’t affect the locations of the points generated by the textToPoints functions since we pass the font size in the parameters.

  • If you intent is to draw points rather than circles or ellipses, I prefer using the point() function since it’s more clear and faster if you don’t need stroke.

So the code might go like this :

let grotesk;
const fontSize = 200;
const nPoints = 20;

let zeroArray;
let fourArray;

function preload() {
  grotesk = loadFont('grotesk.otf');
}

function setup() {
  createCanvas(windowWidth, windowHeight);

  textFont(grotesk);

  const options = {
    sampleFactor: 20,
    simplifyThreshold: 0
  }

  zeroArray = grotesk.textToPoints("0", 100, 300, fontSize, options);
  fourArray = grotesk.textToPoints("4", 500, 300, fontSize, options);

  console.log(zeroArray.length);
  console.log(fourArray.length);

  zeroArray = zeroArray.slice(0, nPoints);
  fourArray = fourArray.slice(0, nPoints);
}

function draw() {
  background(220);

  zero();

  noLoop();
}

function zero() {
  strokeWeight(5);

  for (let i = 0; i < zeroArray.length; i++) {
    const pt = zeroArray[i];

    stroke(random(255));
    point(pt.x, pt.y);
  }

  for (let i = 0; i < fourArray.length; i++) {
    const pt = fourArray[i];

    stroke(random(20));
    point(pt.x, pt.y);
  }
}

As you can see, I printed the number of points generated by the textToPoints function. With a fontSize of 40 this is the result :

zeroArray.length = 2953
fourArray.length = 3402

This is quite large! (100 is too high for the detail)
And the output on the screen only shows two points at the same location.

But my solution is not right because if you take the first 20 points of an array of 2953, you only get the beginning of the contour of the letter. Because the number of points is large, we only get a tiny portion of the first points.

So the way to go is to take 20 points at equal interval in the array of points :

/**
 * Take nElements from array at constant interval
 */
function sampleFromArray(array, nElements) {
  const result = [];
  const increment = Math.ceil(array.length / nElements);

  for (let i = 0; i < array.length; i += increment) {
    result.push(array[i]);
  }

  return result;
}

So we are an array and a number of elements to sample and we first compute the increment between each elements in the array, for example :

array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; // length 10
nElements = 3;

increment = Math.ceil(10 / 3) = Math.ceil(3.333333...) = 4

// So we take
array[0], array[4], array[8]

We use Math.ceil() because it takes the lowest integer superior or equal to a certain number. But we could also use Math.floor() or Math.round().

Note that the function is not really exact since if we wanted to take 7 elements from that array, the increment would be Math.ceil(10 / 7) = 2 so the result wouldbe an array of 8 elements.

It doesn’t matter since we are dealing with large arrays anyway…

So now you can call :

zeroArray = sampleFromArray(zeroArray, nPoints);
fourArray = sampleFromArray(fourArray, nPoints);