fogón

← Asian3D assetfogon/koinobori

Koinobori

by fogonCC03029 blocks
KoinoboriDrag to turn it
// «Koinobori» · gpt-6-sol (medium)
// A tall wooden pole with carp streamers: three fish-shaped windsocks hanging from the pole one above the other (black at the top, red in the middle, blue at the bottom), each with big round eyes and scale patterns, with a small spinning wheel at the top of the pole.

// Stone footing and red-lacquered pole.
box(11, 21, 0, 17, 27, 1, "#d8d5c8");
box(12, 22, 2, 16, 26, 3, "#f0ead7");
box(13, 23, 4, 15, 25, 42, "#a82d2b");
box(13, 22, 4, 15, 22, 42, "#ce4840");
box(13, 23, 4, 13, 25, 42, "#7e2628");
box(14, 22, 5, 14, 22, 41, "#e0664b");

for (const z of [5, 13, 22, 31, 40]) {
  box(12, 22, z, 16, 26, z, "#d9aa50");
  box(13, 21, z, 15, 21, z, "#f4d378");
}
box(12, 22, 42, 16, 26, 42, "#e4ba60");

function carp(name, cz, speed, p) {
  part(name, [14, 24, cz]);

  // Two cords and a short crosspiece tie the open mouth to the pole.
  for (const dz of [-2, 2]) {
    line(15, 24, cz + dz, 19, 24, cz + dz, 0, "#d6ad70");
  }
  box(17, 23, cz - 2, 17, 25, cz + 2, "#e5bc72");

  function radii(x) {
    if (x <= 25) return [3.35, 3.3];
    const t = (x - 25) / 13;
    return [3.35 - 1.85 * t, 3.3 - 1.9 * t];
  }

  // A solid, gently tapering windsock; the lighter upper side remains
  // readable even when the fish swings.
  for (let x = 19; x <= 38; x++) {
    const [ry, rz] = radii(x);
    for (let y = 20; y <= 28; y++) {
      for (let z = cz - 4; z <= cz + 4; z++) {
        const dy = (y - 24) / ry;
        const dz = (z - cz) / rz;
        if (dy * dy + dz * dz > 1) continue;

        let color = p.main;
        if (z >= cz + 2) color = p.light;
        if (z <= cz - 2) color = p.dark;
        if (z <= cz - 2 && y <= 23 && x < 36) color = p.belly;
        if (rand() < 0.045) color = z > cz ? p.light : p.dark;
        set(x, y, z, color);
      }
    }
  }

  // The dark opening is framed by a thick, rounded lip.
  for (let y = 22; y <= 26; y++) {
    for (let dz = -2; dz <= 2; dz++) {
      if (Math.abs(y - 24) === 2 || Math.abs(dz) === 2) {
        set(18, y, cz + dz, p.lip);
      } else {
        set(18, y, cz + dz, "#34282a");
      }
    }
  }

  // Repeated small scallops across the visible flank.
  function frontY(x, z) {
    const [ry, rz] = radii(x);
    const q = Math.abs(z - cz) / rz;
    if (q > 1) return null;
    return Math.ceil(24 - ry * Math.sqrt(1 - q * q));
  }
  for (let x = 27; x <= 36; x += 3) {
    for (let dz = -1; dz <= 2; dz += 3) {
      for (const [dx, rise] of [[-1, 1], [0, 0], [1, 1]]) {
        const xx = x + dx, zz = cz + dz + rise;
        const yy = frontY(xx, zz);
        if (yy !== null) set(xx, yy - 1, zz, p.scale);
      }
    }
  }

  // Curved gill and a large, high-contrast eye on the front cheek.
  for (const [x, dz] of [[25, 2], [26, 1], [26, 0], [25, -1]]) {
    const yy = frontY(x, cz + dz);
    if (yy !== null) set(x, yy - 1, cz + dz, p.lip);
  }
  for (let x = 21; x <= 24; x++) {
    for (let dz = 0; dz <= 3; dz++) {
      if ((x - 22.5) ** 2 + (dz - 1.5) ** 2 <= 3.3)
        set(x, 19, cz + dz, p.lip);
    }
  }
  box(22, 18, cz + 1, 23, 18, cz + 2, "#fff4db");
  set(22, 17, cz + 1, "#19212a");
  set(23, 17, cz + 2, "#19212a");
  set(22, 16, cz + 2, "#ffffff");

  // Dorsal and lower fins, edged in gold.
  for (const side of [-1, 1]) {
    for (let x = 26; x <= 33; x++) {
      const height = x < 30 ? 5 : 4;
      for (let h = 3; h <= height; h++) {
        if (x > 30 && h === 5) continue;
        for (let y = 23; y <= 25; y++) {
          set(x, y, cz + side * h, h === height ? p.lip : p.fin);
        }
      }
    }
  }

  // Two separated, flaring lobes give the tail its unmistakable fish shape.
  for (let x = 39; x <= 44; x++) {
    const spread = Math.round((x - 38) * 0.55);
    for (const side of [-1, 1]) {
      for (let dz = -1; dz <= 1; dz++) {
        for (let y = 23; y <= 25; y++) {
          if (x >= 43 && y !== 24 && dz !== 0) continue;
          set(x, y, cz + side * spread + dz,
              Math.abs(dz) === 1 ? p.lip : p.fin);
        }
      }
    }
  }

  animate(name, { type: "swing", axis: "z", speed, amplitude: 0.3 });
}

carp("black carp", 34, 3, {
  main: "#30343b", light: "#656871", dark: "#1b2028",
  belly: "#a8a49a", scale: "#aaa99a", fin: "#555961", lip: "#c6a665"
});
carp("red carp", 25, 3.5, {
  main: "#c53536", light: "#ef6652", dark: "#8d2430",
  belly: "#f2d8b1", scale: "#ffd08a", fin: "#e64c41", lip: "#e9b85c"
});
carp("blue carp", 16, 4, {
  main: "#327fa7", light: "#67bbcd", dark: "#215579",
  belly: "#e4e8d5", scale: "#ace2d5", fin: "#48a2bc", lip: "#e5bd68"
});

// Gilded wheel, mounted ahead of the pole so its silhouette stays clear.
part("top wheel", [14, 20, 42]);
for (let i = 0; i < 32; i++) {
  const a = i * Math.PI * 2 / 32;
  const x = Math.round(14 + 3 * Math.cos(a));
  const z = Math.round(42 + 3 * Math.sin(a));
  set(x, 19, z, "#e4b758");
  set(x, 20, z, "#a96332");
}
for (let i = 0; i < 8; i++) {
  const a = i * Math.PI / 4;
  line(14, 19, 42,
       14 + 3 * Math.cos(a), 19, 42 + 3 * Math.sin(a),
       0, i % 2 ? "#f1cf75" : "#b43832");
}
box(13, 18, 41, 15, 18, 43, "#dba94e");
set(14, 17, 42, "#fff0a6");
animate("top wheel", { type: "spin", axis: "y", speed: 2.5 });