← Basics3D assetfogon/jarra
Jarra
Drag to turn it// «Jarra» · gpt-6-sol (medium)
// Pitcher («Jarra»).
const cx = 20, cy = 24;
const profile = [
[0, 7.5], [2, 9], [5, 10.5], [9, 12],
[15, 13.5], [20, 14], [24, 13],
[28, 11], [31, 9.5], [35, 9.5],
[37, 10.5], [39, 12], [40, 12]
];
function radius(z) {
for (let i = 1; i < profile.length; i++) {
if (z <= profile[i][0]) {
const [z0, r0] = profile[i - 1];
const [z1, r1] = profile[i];
return r0 + (r1 - r0) * (z - z0) / (z1 - z0);
}
}
return 12;
}
const teal = ["#17686b", "#207b7d", "#28898a", "#329494"];
const cream = ["#c5b99a", "#d9ceb0", "#e7dec3"];
for (let z = 0; z <= 40; z++) {
const rx = radius(z);
const ry = rx * 0.86;
for (let x = 5; x <= 35; x++) {
for (let y = 10; y <= 38; y++) {
const dx = (x - cx) / rx, dy = (y - cy) / ry;
const d = dx * dx + dy * dy;
const flute = z >= 5 && z <= 33
? 0.025 * Math.cos(Math.atan2(y - cy, x - cx) * 18)
: 0;
if (d > 1 + flute) continue;
// The mouth is genuinely open, with a deep, shaded interior.
if (z >= 32) {
const inner = z < 37 ? 0.67 : 0.63;
if (d < inner) continue;
}
let color;
if (z >= 39) {
color = y < cy ? cream[2] : cream[1];
if (rand() < 0.15) color = cream[0];
} else if (z === 38 && d > 0.78) {
color = y < cy ? cream[1] : cream[0];
} else if (z >= 32 && d < 0.82) {
color = y < cy ? "#a99e82" : "#bcb092";
} else {
const ridge = Math.cos(Math.atan2(y - cy, x - cx) * 18);
color = ridge > 0.38 ? teal[2] : ridge < -0.45 ? teal[0] : teal[1];
if (z > 25 && y < cy) color = ridge > 0 ? teal[2] : teal[1];
if (z < 5) color = teal[0];
if (rand() < 0.075) color = teal[Math.floor(rand() * teal.length)];
}
set(x, y, z, color);
}
}
}
// Sandy floor of the hollow mouth and its irregular, visible inner markings.
for (let x = 13; x <= 27; x++)
for (let y = 18; y <= 30; y++)
if (((x-cx)/7.3)**2 + ((y-cy)/5.9)**2 < 1)
set(x, y, 31, rand() < 0.22 ? "#8e876e" : "#b8ad8d");
for (let x = 17; x <= 24; x++) {
box(x, 22 + (x % 3), 32, x, 22 + (x % 3), 35,
x % 2 ? "#857d67" : "#a99d7f");
}
box(18, 25, 34, 22, 26, 34, "#c8bd9d");
// Thick, uneven pale lip and the little projecting pouring spout on the left.
for (let x = 7; x <= 13; x++) {
const y0 = 18 + Math.floor((13 - x) / 3);
box(x, y0, 38, x, 25, 38, "#d7ceb3");
if (x <= 10) box(x, y0, 39, x, 24, 39, "#eee6cd");
}
box(6, 20, 37, 9, 24, 37, "#287f80");
box(7, 20, 38, 10, 22, 38, "#f0e9d5");
// Front-facing surface details follow the curve of the vessel.
function front(x, z, color, raised = false) {
let y = 24;
while (y > 8 && !get(x, y, z)) y--;
if (y > 8) set(x, raised ? y - 1 : y, z, color);
}
// Long, subtly broken ceramic flutes.
for (let x = 10; x <= 31; x += 3) {
for (let z = 8; z <= 29; z++) {
if (rand() < 0.14 || (z > 23 && x < 14)) continue;
front(x, z, z % 5 === 0 ? "#358f8c" : "#155f64");
if (x + 1 <= 31 && z % 4 !== 0)
front(x + 1, z, "#349494");
}
}
for (let x = 12; x <= 30; x++)
if (rand() < 0.75) front(x, 4 + Math.floor(rand() * 2), "#adab8e");
// Small cream-colored painted motifs on the belly.
for (const [x, z, c] of [
[15,19,"#c5b99a"],[16,20,"#e5d9b8"],[17,20,"#e9ddbf"],
[18,19,"#d5c8a7"],[19,18,"#b6a988"],
[23,24,"#ddd3b6"],[24,25,"#e9e0c6"],[25,25,"#e9e0c6"],
[26,25,"#d9ceb0"],[27,24,"#e5dbc0"],
[23,23,"#e6ddc3"],[27,23,"#e4d9bb"],
[24,22,"#b4a478"],[25,22,"#c49d57"],[26,22,"#b7894d"],
[23,21,"#d5c7a4"],[24,20,"#eadfc1"],[25,20,"#e9ddbc"],
[26,20,"#d8c9a5"],[28,22,"#e8ddbf"],
[29,20,"#ddd0ac"],[29,19,"#d9caa7"],[30,21,"#eadfc2"]
]) front(x, z, c, true);
// The handle sits behind the right shoulder: a chunky angular D-shaped loop.
const handlePath = [
[29, 29, 36], [35, 29, 38], [39, 29, 38],
[42, 29, 35], [43, 29, 32], [43, 29, 25],
[41, 29, 20], [37, 29, 17], [32, 29, 17]
];
for (let i = 1; i < handlePath.length; i++) {
const a = handlePath[i-1], b = handlePath[i];
line(...a, ...b, 1.8, "#176b70");
}
line(31, 27, 37, 37, 27, 39, 0.8, "#3b9b9a");
line(39, 27, 38, 41, 27, 35, 0.8, "#329494");
line(42, 27, 34, 42, 27, 26, 0.8, "#2b888a");
line(41, 27, 23, 38, 27, 19, 0.8, "#268082");
box(30, 27, 34, 33, 30, 36, "#238184");
box(31, 27, 16, 35, 30, 19, "#1c7175");import { draw } from "./assets/voxel";
import { jarra } from "./assets/jarra";
// Every frame: standing on (x, y), the centre of its base.
draw(ctx, jarra, x, y, { size: 64, angle: 0, t: performance.now() / 1000 });
// size: its longest side, in px
// angle: turn on the floor, in radians
// t: seconds; if it moves, that animates it# In a terminal, in the game's folder
fogon assets add jarra
# With Fogón's tools (connector)
add_assets({ slug: "<your-game>", ids: ["jarra"] })
# It lands in client/assets/jarra.ts
# https://fogon.page/object/jarra