← Fantasy3D assetfogon/portal-de-piedra
Portal de piedra
Drag to turn it// «Portal de piedra» · gpt-6-sol (medium)
// A tall ancient stone portal: a big round-topped archway of mossy grey stone blocks on a raised stepped base, carved runes along the arch, two little torches on the sides, and filling the opening a swirling flat disc of glowing magic energy, violet at the edge and cyan-white at the center.
const stone = ["#626c70", "#737d7d", "#86908b", "#9aa39a"];
const dark = "#465257";
const moss = ["#536c58", "#66805e", "#819465"];
const gold = "#bda36a";
// Wide, stepped foundation and the little stair leading into the portal.
box(4, 14, 0, 44, 34, 0, dark);
box(5, 15, 1, 43, 33, 1, stone[0]);
box(7, 17, 2, 41, 31, 2, stone[1]);
box(8, 18, 3, 40, 30, 3, stone[2]);
for (let i = 0; i < 3; i++) {
box(17 + i, 11 + i * 2, i, 31 - i, 18, i, stone[i]);
box(17 + i, 11 + i * 2, i, 31 - i, 11 + i * 2, i, stone[Math.min(i + 1, 3)]);
}
box(14, 19, 4, 34, 29, 4, stone[0]);
box(15, 18, 4, 33, 18, 4, gold);
// Solid square piers, with a round-topped stone arch above them.
for (let z = 4; z <= 44; z++) {
for (let x = 6; x <= 42; x++) {
const dx = x - 24, dz = z - 26;
const d = Math.hypot(dx, dz);
const pier = z <= 26 && ((x >= 7 && x <= 14) || (x >= 34 && x <= 41));
const arch = z >= 26 && d >= 10.2 && d <= 17.8;
if (!pier && !arch) continue;
for (let y = 19; y <= 29; y++) {
let c = y === 19 ? stone[2] : y === 29 ? stone[0] : stone[1];
if (rand() < 0.11) c = stone[Math.floor(rand() * stone.length)];
// Recessed, irregular mortar joints on the visible face.
if (y === 19) {
if (pier) {
if ((z - 4) % 5 === 0 ||
(x + (Math.floor(z / 5) % 2 ? 2 : 0)) % 6 === 0) c = dark;
} else {
const angle = Math.atan2(dz, dx);
const joint = (angle + 0.025) % (Math.PI / 11);
if (joint < 0.035 || Math.abs(d - 14) < 0.35) c = dark;
}
}
set(x, y, z, c);
}
}
}
// Raised corner stones, inset panels, and bands around the piers.
for (const x of [7, 14, 34, 41]) {
for (let z = 5; z <= 25; z++) {
if (x === 7 || x === 41) set(x, 18, z, z % 5 === 0 ? dark : stone[3]);
if (x === 14 || x === 34) set(x, 18, z, stone[3]);
}
}
for (const z of [5, 12, 25]) {
for (let x = 7; x <= 14; x++) set(x, 18, z, stone[3]);
for (let x = 34; x <= 41; x++) set(x, 18, z, stone[3]);
}
box(9, 18, 8, 12, 18, 10, dark);
box(36, 18, 8, 39, 18, 10, dark);
set(10, 17, 9, gold); set(11, 17, 9, gold);
set(37, 17, 9, gold); set(38, 17, 9, gold);
// A pale carved line following the inside of the arch.
for (let a = 0; a <= Math.PI; a += 0.025) {
const x = Math.round(24 + 10.7 * Math.cos(a));
const z = Math.round(26 + 10.7 * Math.sin(a));
set(x, 18, z, stone[3]);
}
// Individual luminous runes, spaced like inscriptions on the arch stones.
for (let i = 0; i < 10; i++) {
const a = 0.16 + i * (Math.PI - 0.32) / 9;
const x = Math.round(24 + 14.5 * Math.cos(a));
const z = Math.round(26 + 14.5 * Math.sin(a));
const ink = i % 3 === 0 ? "#91e4d6" : "#b3c9ba";
set(x, 18, z, "#e4f5d9");
set(x, 18, z + 1, ink);
set(x, 18, z - 1, ink);
set(x + (i % 2 ? 1 : -1), 18, z, ink);
if (i % 3 === 0) set(x + (i % 2 ? 1 : -1), 18, z + 1, ink);
}
// Moss gathers in crevices and across the crown.
for (let x = 6; x <= 42; x++) {
for (let z = 5; z <= 44; z++) {
if (!get(x, 19, z)) continue;
const crown = z > 37;
const lowCorner = z < 13 && (x < 11 || x > 37);
if ((crown || lowCorner) && rand() < (crown ? 0.27 : 0.19))
set(x, 18, z, moss[Math.floor(rand() * moss.length)]);
}
}
for (let x = 9; x <= 39; x++) {
if (get(x, 20, 43) && rand() < 0.6) set(x, 20, 44, moss[1]);
}
// Little storybook mushrooms at the feet of the portal.
for (const [x, y, z] of [[8, 16, 3], [11, 17, 3], [39, 17, 3], [42, 16, 2]]) {
set(x, y, z, "#e9dec2");
box(x - 1, y - 1, z + 1, x + 1, y + 1, z + 1, "#bb6480");
set(x, y - 1, z + 2, "#eec1bc");
set(x - 1, y - 1, z + 1, "#f3d4bf");
}
// Brass wall brackets and dark wooden torch handles.
for (const x of [6, 42]) {
box(x - 1, 15, 15, x + 1, 19, 16, dark);
box(x - 1, 15, 16, x + 1, 17, 17, gold);
box(x, 14, 17, x, 16, 20, "#604a42");
box(x - 1, 14, 19, x + 1, 16, 19, "#d5ae63");
}
// The opening is a thin, solid disc: violet rim, whorls of magenta,
// and an intensely bright cyan-white heart.
part("portal-energy", [24, 20, 21]);
for (let z = 5; z <= 36; z++) {
for (let x = 14; x <= 34; x++) {
const dx = x - 24, dz = z - 26;
const inside = z <= 26 ? Math.abs(dx) <= 9.3
: Math.hypot(dx, dz) <= 9.8;
if (!inside) continue;
const nx = dx / 10;
const nz = (z - 20) / 17;
const radius = Math.sqrt(nx * nx + nz * nz);
const angle = Math.atan2(nz, nx);
const swirl = Math.sin(angle * 2.7 + radius * 12 - z * 0.12);
let c = radius > 0.83 ? "#762cbe"
: radius > 0.64 ? (swirl > 0 ? "#d442dc" : "#963ee5")
: radius > 0.32 ? (swirl > 0.22 ? "#9bfaff" : "#3acde3")
: swirl > -0.3 ? "#eaffff" : "#85f7f5";
for (let y = 20; y <= 21; y++) set(x, y, z, c);
if (swirl > 0.72 && radius < 0.7) set(x, 19, z, "#c5ffff");
}
}
animate("portal-energy", {
type: "colors", palette: ["#722dc6", "#d443de", "#4de9f2", "#ecffff"],
pattern: "wave", speed: 2, glow: 1
});
// Both flames flicker independently above their small torch cups.
for (const [name, x] of [["left-flame", 6], ["right-flame", 42]]) {
part(name, [x, 15, 20]);
ellipsoid(x, 15, 21, 2, 2, 2, "#ff952e");
ellipsoid(x, 15, 23, 1, 1, 2, "#ffd35c");
set(x, 15, 25, "#fff0a1");
animate(name, {
type: "colors", palette: ["#f65c24", "#ff9b2d", "#ffdc5c", "#fff1a0"],
pattern: "flicker", speed: 7, glow: 0.9
});
}import { draw } from "./assets/voxel";
import { portalDePiedra } from "./assets/portal-de-piedra";
// Every frame: standing on (x, y), the centre of its base.
draw(ctx, portalDePiedra, 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 portal-de-piedra
# With Fogón's tools (connector)
add_assets({ slug: "<your-game>", ids: ["portal-de-piedra"] })
# It lands in client/assets/portal-de-piedra.ts
# https://fogon.page/object/portal-de-piedra