260419

const W = 800;
const H = 800;
const { animate } = anime;
let animeValue = {
  count: 0,
};

let cycle = 0;
let palette = [];
let currentSeed = 0;
const SIZE = 700;
const MIN_SIZE = 120;
const STROKE_W = 2;
const FONT_URL = "fonts/WDXLLubrifontJPN-Regular.ttf";
let myFont;
const REGION_TEXTS = ["焼", "肉", "定", "食"];

async function setup() {
  createCanvas(W, H, WEBGL);
  myFont = await loadFont(FONT_URL);
  strokeCap(SQUARE);
  strokeWeight(STROKE_W);
  palette = colorArray[0].colors;
  background(palette[4]);
  animate(animeValue, {
    count: 1,
    duration: 1500,
    ease: "inOutExpo",
    loop: true,
    onLoop: () => {
      cycle++;
    },
  });
  currentSeed = int(random(10000));
}

function draw() {
  background(palette[4]);
  ortho();
  // orbitControl();
  rotateX(PI / 10);
  rotateY(-PI / 10);

  let rawCur = rawForCycle(cycle);
  let x1, y1, x2, y2;
  if (cycle === 0) {
    x1 = rawCur[0];
    y1 = rawCur[1];
    x2 = rawCur[2];
    y2 = rawCur[3];
  } else {
    let rawPrev = rawForCycle(cycle - 1);
    x1 = rawPrev[2];
    y1 = rawPrev[3];
    x2 = rawCur[2];
    y2 = rawCur[3];
  }

  let lineFrame = animeValue.count;
  let x = lerp(x1, x2, lineFrame);
  let y = lerp(y1, y2, lineFrame);

  const L = -SIZE / 2 - STROKE_W;
  const R = SIZE / 2 - STROKE_W;
  const B = -SIZE / 2 - STROKE_W;
  const T = SIZE / 2 - STROKE_W;

  const regions = [
    { cx: (L + x) / 2, cy: (B + y) / 2, w: x - L, h: y - B },
    { cx: (x + R) / 2, cy: (B + y) / 2, w: R - x, h: y - B },
    { cx: (L + x) / 2, cy: (y + T) / 2, w: x - L, h: T - y },
    { cx: (x + R) / 2, cy: (y + T) / 2, w: R - x, h: T - y },
  ];

  for (let i = 0; i < regions.length; i++) {
    const r = regions[i];
    drawTextFitCell(REGION_TEXTS[i], r.cx, r.cy, r.w, r.h);
  }
}

function drawTextFitCell(str, cx, cy, cellW, cellH) {
  push();
  textFont(myFont);
  const innerW = max(1, cellW - 2 * STROKE_W);
  const innerH = max(1, cellH - 2 * STROKE_W);
  const ts = sqrt(innerW * innerH);
  textSize(ts);
  const gb = glyphPathAndBounds(str);
  if (!gb) {
    pop();
    return;
  }
  const { commands, g } = gb;
  const cxBox = g.x + g.w / 2;
  const cyBox = g.y + g.h / 2;
  translate(cx, cy, 0.01);
  scale(innerW / g.w, innerH / g.h);
  translate(-cxBox, -cyBox);

  push();
  fill(palette[2]);
  noStroke();
  drawP5PathCommandsFilled(commands);
  pop();

  pop();
}

function rawForCycle(cycle) {
  randomSeed(currentSeed + cycle);
  return [
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
  ];
}
// The following code is generated.

/*
 * 【何をしているか】
 *   loadFont したフォントから textToPaths で輪郭データを取り、WEBGL の beginShape で塗りつぶす。
 *   p5 v2(beta)の Typography API を使う(stable 1.x には textToPaths がない)。
 *
 * 【textToPaths の形】
 *   フラットな配列。各要素は [種別, 数値…]。
 *   種別は M / L / Q / C / Z(SVG のパスに近い)。Z は「ここまでがひとかたまりの閉じた線」。
 *
 * 【セルに合わせるための箱】
 *   グリフ全体の幅・高さがほしいので、すべての座標から軸に平行な外接矩形(AABB)を求める。
 *   ベジェの制御点だけを見ているので、曲線がはみ出す場合は箱が実線より小さくなることがある。
 *
 * 【塗り方】
 *   Z までを 1 輪郭として順に描く。輪郭が複数ある文字(「口」の内側など)は、2 本目から
 *   beginContour / endContour で「穴」として追加する。
 *   beginShape() に TESS などモードを付けると bezierVertex が使えないので、モードなしだけ使う。
 *
 * 参考:
 *   textToPaths — https://beta.p5js.org/reference/p5.Font/textToPaths
 *   beginShape — https://beta.p5js.org/reference/p5/beginshape/
 *   beginContour — https://beta.p5js.org/reference/p5/beginContour/
 *   bezierOrder — https://beta.p5js.org/reference/p5/bezierorder/
 *   bezierVertex — https://beta.p5js.org/reference/p5/bezierVertex/
 */

// textToPaths の結果と、セルに合わせる用の外接矩形(各コマンドの数値を (x,y) ペアとして走査)
function glyphPathAndBounds(str) {
  if (!myFont || typeof myFont.textToPaths !== "function") return null;
  const commands = myFont.textToPaths(str, 0, 0);
  if (!commands?.length) return null;
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (const cmd of commands) {
    for (let i = 1; i + 1 < cmd.length; i += 2) {
      const x = cmd[i];
      const y = cmd[i + 1];
      if (!isFinite(x) || !isFinite(y)) continue;
      minX = Math.min(minX, x);
      maxX = Math.max(maxX, x);
      minY = Math.min(minY, y);
      maxY = Math.max(maxY, y);
    }
  }
  if (!isFinite(minX) || maxX <= minX || maxY <= minY) return null;
  const w = maxX - minX;
  const h = maxY - minY;
  if (!w || !h) return null;
  return { commands, g: { x: minX, y: minY, w, h } };
}

// パス配列をそのまま読み、Z または最後までで輪郭を切り替えて塗る(中間配列は作らない)
function drawP5PathCommandsFilled(commands) {
  const n = commands.length;
  if (!n) return;

  const prevOrder =
    typeof bezierOrder === "function" ? bezierOrder() : undefined;

  let outer = true;
  beginShape();
  for (let i = 0; i < n; ) {
    let j = i;
    while (j < n && commands[j][0] !== "Z") j++;
    const closed = j < n;
    const end = closed ? j + 1 : n;
    if (!outer) beginContour();
    for (let k = i; k < end; k++) {
      const cmd = commands[k];
      switch (cmd[0]) {
        case "M":
        case "L":
          vertex(cmd[1], cmd[2]);
          break;
        case "C":
          bezierOrder(3);
          bezierVertex(cmd[1], cmd[2]);
          bezierVertex(cmd[3], cmd[4]);
          bezierVertex(cmd[5], cmd[6]);
          break;
        case "Q":
          bezierOrder(2);
          bezierVertex(cmd[1], cmd[2]);
          bezierVertex(cmd[3], cmd[4]);
          break;
        default:
          break;
      }
    }
    if (!outer) endContour();
    outer = false;
    i = closed ? j + 1 : n;
  }
  endShape(CLOSE);

  if (typeof bezierOrder === "function" && prevOrder !== undefined) {
    bezierOrder(prevOrder);
  }
}

const colorArray = [
  {
    id: 0,
    colors: [
      "#9dbdba",
      "#f8b042",
      "#e47763",
      "#253276",
      "#dfdad3",
      "#FFFFFF",
      "#000000",
    ],
  },
];

textToPoints()で何ができるかを模索中。

const W = 800;
const H = 800;
const { animate } = anime;
let animeValue = {
  count: 0,
};

let cycle = 0;
let palette = [];
let currentSeed = 0;
const SIZE = 700;
const MIN_SIZE = 120;
const STROKE_W = 2;
const FONT_URL = "fonts/WDXLLubrifontJPN-Regular.ttf";
let myFont;
const REGION_TEXTS = ["焼", "肉", "定", "食"];

async function setup() {
  createCanvas(W, H, WEBGL);
  myFont = await loadFont(FONT_URL);
  strokeCap(SQUARE);
  strokeWeight(STROKE_W);
  palette = colorArray[0].colors;
  background(palette[4]);
  animate(animeValue, {
    count: 1,
    duration: 1500,
    ease: "inOutExpo",
    loop: true,
    onLoop: () => {
      cycle++;
    },
  });
  currentSeed = int(random(10000));
}

function draw() {
  background(palette[4]);
  ortho();
  // orbitControl();
  rotateX(PI / 10);
  rotateY(-PI / 10);

  let rawCur = rawForCycle(cycle);
  let x1, y1, x2, y2;
  if (cycle === 0) {
    x1 = rawCur[0];
    y1 = rawCur[1];
    x2 = rawCur[2];
    y2 = rawCur[3];
  } else {
    let rawPrev = rawForCycle(cycle - 1);
    x1 = rawPrev[2];
    y1 = rawPrev[3];
    x2 = rawCur[2];
    y2 = rawCur[3];
  }

  let lineFrame = animeValue.count;
  let x = lerp(x1, x2, lineFrame);
  let y = lerp(y1, y2, lineFrame);

  const L = -SIZE / 2 - STROKE_W;
  const R = SIZE / 2 - STROKE_W;
  const B = -SIZE / 2 - STROKE_W;
  const T = SIZE / 2 - STROKE_W;

  const regions = [
    { cx: (L + x) / 2, cy: (B + y) / 2, w: x - L, h: y - B },
    { cx: (x + R) / 2, cy: (B + y) / 2, w: R - x, h: y - B },
    { cx: (L + x) / 2, cy: (y + T) / 2, w: x - L, h: T - y },
    { cx: (x + R) / 2, cy: (y + T) / 2, w: R - x, h: T - y },
  ];

  for (let i = 0; i < regions.length; i++) {
    const r = regions[i];
    drawTextFitCell(REGION_TEXTS[i], r.cx, r.cy, r.w, r.h);
  }
}

function drawTextFitCell(str, cx, cy, cellW, cellH) {
  push();
  textFont(myFont);
  const innerW = max(1, cellW - 2 * STROKE_W);
  const innerH = max(1, cellH - 2 * STROKE_W);
  const ts = sqrt(innerW * innerH);
  textSize(ts);
  const gb = glyphPathAndBounds(str);
  if (!gb) {
    pop();
    return;
  }
  const { commands, g } = gb;
  const cxBox = g.x + g.w / 2;
  const cyBox = g.y + g.h / 2;
  translate(cx, cy, 0.01);
  scale(innerW / g.w, innerH / g.h);
  translate(-cxBox, -cyBox);

  push();
  fill(palette[2]);
  noStroke();
  drawP5PathCommandsFilled(commands);
  pop();

  pop();
}

function rawForCycle(cycle) {
  randomSeed(currentSeed + cycle);
  return [
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
    random(-SIZE / 2 + MIN_SIZE, SIZE / 2 - MIN_SIZE),
  ];
}
// The following code is generated.

/*
 * 【何をしているか】
 *   loadFont したフォントから textToPaths で輪郭データを取り、WEBGL の beginShape で塗りつぶす。
 *   p5 v2(beta)の Typography API を使う(stable 1.x には textToPaths がない)。
 *
 * 【textToPaths の形】
 *   フラットな配列。各要素は [種別, 数値…]。
 *   種別は M / L / Q / C / Z(SVG のパスに近い)。Z は「ここまでがひとかたまりの閉じた線」。
 *
 * 【セルに合わせるための箱】
 *   グリフ全体の幅・高さがほしいので、すべての座標から軸に平行な外接矩形(AABB)を求める。
 *   ベジェの制御点だけを見ているので、曲線がはみ出す場合は箱が実線より小さくなることがある。
 *
 * 【塗り方】
 *   Z までを 1 輪郭として順に描く。輪郭が複数ある文字(「口」の内側など)は、2 本目から
 *   beginContour / endContour で「穴」として追加する。
 *   beginShape() に TESS などモードを付けると bezierVertex が使えないので、モードなしだけ使う。
 *
 * 参考:
 *   textToPaths — https://beta.p5js.org/reference/p5.Font/textToPaths
 *   beginShape — https://beta.p5js.org/reference/p5/beginshape/
 *   beginContour — https://beta.p5js.org/reference/p5/beginContour/
 *   bezierOrder — https://beta.p5js.org/reference/p5/bezierorder/
 *   bezierVertex — https://beta.p5js.org/reference/p5/bezierVertex/
 */

// textToPaths の結果と、セルに合わせる用の外接矩形(各コマンドの数値を (x,y) ペアとして走査)
function glyphPathAndBounds(str) {
  if (!myFont || typeof myFont.textToPaths !== "function") return null;
  const commands = myFont.textToPaths(str, 0, 0);
  if (!commands?.length) return null;
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (const cmd of commands) {
    for (let i = 1; i + 1 < cmd.length; i += 2) {
      const x = cmd[i];
      const y = cmd[i + 1];
      if (!isFinite(x) || !isFinite(y)) continue;
      minX = Math.min(minX, x);
      maxX = Math.max(maxX, x);
      minY = Math.min(minY, y);
      maxY = Math.max(maxY, y);
    }
  }
  if (!isFinite(minX) || maxX <= minX || maxY <= minY) return null;
  const w = maxX - minX;
  const h = maxY - minY;
  if (!w || !h) return null;
  return { commands, g: { x: minX, y: minY, w, h } };
}

// パス配列をそのまま読み、Z または最後までで輪郭を切り替えて塗る(中間配列は作らない)
function drawP5PathCommandsFilled(commands) {
  const n = commands.length;
  if (!n) return;

  const prevOrder =
    typeof bezierOrder === "function" ? bezierOrder() : undefined;

  let outer = true;
  beginShape();
  for (let i = 0; i < n; ) {
    let j = i;
    while (j < n && commands[j][0] !== "Z") j++;
    const closed = j < n;
    const end = closed ? j + 1 : n;
    if (!outer) beginContour();
    for (let k = i; k < end; k++) {
      const cmd = commands[k];
      switch (cmd[0]) {
        case "M":
        case "L":
          vertex(cmd[1], cmd[2]);
          break;
        case "C":
          bezierOrder(3);
          bezierVertex(cmd[1], cmd[2]);
          bezierVertex(cmd[3], cmd[4]);
          bezierVertex(cmd[5], cmd[6]);
          break;
        case "Q":
          bezierOrder(2);
          bezierVertex(cmd[1], cmd[2]);
          bezierVertex(cmd[3], cmd[4]);
          break;
        default:
          break;
      }
    }
    if (!outer) endContour();
    outer = false;
    i = closed ? j + 1 : n;
  }
  endShape(CLOSE);

  if (typeof bezierOrder === "function" && prevOrder !== undefined) {
    bezierOrder(prevOrder);
  }
}

const colorArray = [
  {
    id: 0,
    colors: [
      "#9dbdba",
      "#f8b042",
      "#e47763",
      "#253276",
      "#dfdad3",
      "#FFFFFF",
      "#000000",
    ],
  },
];

Last Updated:

260419