← Parties and Christmas3D assetfogon/pan-dulce
Pan dulce
Drag to turn it// «Pan dulce» · gpt-6-sol (medium)
// A round Argentinian pan dulce on a wooden board: a tall domed golden-brown loaf with a cracked top dusted with a few nuts, its sides studded with bits of red and green candied fruit and dark raisins, with one slice cut out showing the yellow crumb inside.
// Rounded wooden serving board
for (let x = 3; x <= 43; x++) {
for (let y = 3; y <= 44; y++) {
const dx = Math.max(7 - x, 0, x - 39);
const dy = Math.max(7 - y, 0, y - 40);
if (dx * dx + dy * dy > 16) continue;
for (let z = 0; z <= 2; z++) {
let color = z === 0 ? "#8d4b28" : z === 1 ? "#b66b36" : "#d8934d";
if (z === 2) {
if (y === 12 || y === 21 || y === 30 || y === 39) color = "#ad6636";
else if (rand() < 0.09) color = "#e2a65f";
}
set(x, y, z, color);
}
}
}
// Small handle with a hanging hole
for (let x = 42; x <= 46; x++) {
for (let y = 21; y <= 27; y++) {
if ((x === 46 && (y === 21 || y === 27)) ||
(x === 42 && (y === 21 || y === 27))) continue;
for (let z = 0; z <= 2; z++)
set(x, y, z, z === 2 ? "#d8934d" : z === 1 ? "#b66b36" : "#8d4b28");
}
}
for (let z = 0; z <= 2; z++) set(44, 24, z, null);
// Soft shadow beneath the loaf
for (let x = 7; x <= 39; x++) {
for (let y = 9; y <= 41; y++) {
const d = ((x - 23) / 17) ** 2 + ((y - 25) / 16) ** 2;
if (d < 1 && get(x, y, 2))
set(x, y, 2, d > 0.83 ? "#b87740" : "#94522c");
}
}
function loafTop(x, y) {
const r2 = ((x - 23) / 15.5) ** 2 + ((y - 25) / 15) ** 2;
return r2 <= 1 ? 16 + 19 * Math.sqrt(1 - r2) : -1;
}
// The loaf has a generous slice removed from its front.
for (let x = 7; x <= 39; x++) {
for (let y = 10; y <= 40; y++) {
const top = loafTop(x, y);
if (top < 5) continue;
for (let z = 4; z <= Math.floor(top); z++) {
if (y < 20 && x >= 18 && x <= 29) continue;
const cutFace = (y === 20 && x >= 18 && x <= 29) ||
(x === 17 && y < 20) || (x === 30 && y < 20);
let color;
if (cutFace) {
color = rand() < 0.13 ? "#edbd62" :
rand() < 0.27 ? "#fff0b1" : "#ffdf83";
} else if (z < 8) {
color = rand() < 0.12 ? "#af632e" : "#bd7032";
} else if (z >= top - 2) {
color = rand() < 0.23 ? "#f5b55f" : "#dc8e42";
} else {
color = rand() < 0.13 ? "#e8a253" : "#c57a37";
}
set(x, y, z, color);
}
}
}
// Speckles and fruit in the exposed, buttery crumb
for (let x = 18; x <= 29; x++) {
for (let z = 7; z <= 30; z++) {
if (!get(x, 20, z) || rand() > 0.075) continue;
const fruit = ["#bd3539", "#287d4b", "#69412d", "#f39b45"];
set(x, 20, z, fruit[Math.floor(rand() * fruit.length)]);
if (rand() < 0.35 && get(x + 1, 20, z))
set(x + 1, 20, z, fruit[Math.floor(rand() * fruit.length)]);
}
}
// Bright candied fruit and raisins studding the outer crust
for (let x = 8; x <= 38; x++) {
for (let y = 11; y <= 39; y++) {
for (let z = 8; z <= 27; z++) {
if (!get(x, y, z) || rand() > 0.031) continue;
const exposed = !get(x, y - 1, z) ||
!get(x - 1, y, z) || !get(x + 1, y, z);
if (!exposed) continue;
if ((y === 20 && x >= 18 && x <= 29) ||
(x === 17 && y < 20) || (x === 30 && y < 20)) continue;
const bits = ["#dc353a", "#f04a46", "#32955a", "#267847", "#61402d"];
set(x, y, z, bits[Math.floor(rand() * bits.length)]);
}
}
}
// An irregular, dark baked crack across the crown, with pale torn edges
for (let y = 17; y <= 34; y++) {
const x = 23 + Math.round(1.5 * Math.sin(y * 0.72));
const top = Math.floor(loafTop(x, y));
if (top < 27) continue;
set(x, y, top, "#925025");
if (get(x + 1, y, top)) set(x + 1, y, top, "#ffe0a0");
if (y % 4 === 0 && get(x - 1, y, top))
set(x - 1, y, top, "#ad602c");
}
// A few chunky toasted nuts nestled into the top
const nuts = [
[17, 25], [19, 31], [27, 29], [29, 22], [24, 18], [32, 27]
];
for (const [x, y] of nuts) {
const z = Math.floor(loafTop(x, y));
if (z < 23) continue;
ellipsoid(x, y, z + 1, 1.5, 1, 0.8, "#f4d18b");
set(x, y, z + 2, "#fff0bc");
}
// The removed slice stands in front, showing its golden crumb.
for (let x = 25; x <= 40; x++) {
const u = (x - 32.5) / 8;
if (Math.abs(u) >= 1) continue;
const top = Math.floor(18 + 8 * Math.sqrt(1 - u * u));
for (let y = 6; y <= 11; y++) {
for (let z = 4; z <= top; z++) {
const crust = z >= top - 2 || z <= 5 ||
x <= 26 || x >= 39;
let color;
if (crust)
color = z >= top - 1 ? "#e7a052" : "#b96d32";
else if (y <= 8)
color = rand() < 0.16 ? "#fff0b0" : "#ffdd7d";
else
color = "#e9b765";
set(x, y, z, color);
}
}
}
// Fruit clearly visible on the slice's cut face
const sliceFruit = [
[29, 11, "#d83d42"], [35, 10, "#338b51"],
[32, 16, "#65402c"], [37, 17, "#d93c42"],
[28, 18, "#329653"], [34, 21, "#62412e"]
];
for (const [x, z, color] of sliceFruit) {
if (get(x, 6, z)) set(x, 6, z, color);
}
// A little shine along the board's front edge
for (let x = 9; x <= 40; x++) {
if (get(x, 3, 2) && x % 5 !== 0) set(x, 3, 2, "#edb36b");
}import { draw } from "./assets/voxel";
import { panDulce } from "./assets/pan-dulce";
// Every frame: standing on (x, y), the centre of its base.
draw(ctx, panDulce, 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 pan-dulce
# With Fogón's tools (connector)
add_assets({ slug: "<your-game>", ids: ["pan-dulce"] })
# It lands in client/assets/pan-dulce.ts
# https://fogon.page/object/pan-dulce