Adding button generator

This commit is contained in:
Dan 2026-01-08 12:04:45 +00:00
parent d22d127cac
commit bdee635df0
11 changed files with 1458 additions and 6 deletions

View file

@ -0,0 +1,594 @@
(function () {
const canvas = document.getElementById("button-canvas");
const ctx = canvas.getContext("2d");
// Preload all web fonts for canvas rendering
const fonts = [
"Lato",
"Roboto",
"Open Sans",
"Montserrat",
"Oswald",
"Bebas Neue",
"Roboto Mono",
"VT323",
"Press Start 2P",
"DSEG7-Classic",
];
// Load fonts using CSS Font Loading API
const fontPromises = fonts.flatMap((font) => [
document.fonts.load(`400 12px "${font}"`),
document.fonts.load(`700 12px "${font}"`),
document.fonts.load(`italic 400 12px "${font}"`),
]);
Promise.all(fontPromises).then(() => {
console.log("All fonts loaded for canvas");
drawButton();
});
// Collapsible sections functionality
document.querySelectorAll(".control-group-header").forEach((header) => {
header.addEventListener("click", () => {
const controlGroup = header.closest(".control-group");
controlGroup.classList.toggle("collapsed");
});
});
// Get all controls
const controls = {
text: document.getElementById("button-text"),
textEnabled: document.getElementById("text-enabled"),
fontSize: document.getElementById("font-size"),
textX: document.getElementById("text-x"),
textY: document.getElementById("text-y"),
textColorType: document.getElementById("text-color-type"),
textColor: document.getElementById("text-color"),
textGradientColor1: document.getElementById("text-gradient-color1"),
textGradientColor2: document.getElementById("text-gradient-color2"),
textGradientAngle: document.getElementById("text-gradient-angle"),
textOutline: document.getElementById("text-outline"),
outlineColor: document.getElementById("outline-color"),
fontFamily: document.getElementById("font-family"),
fontBold: document.getElementById("font-bold"),
fontItalic: document.getElementById("font-italic"),
text2: document.getElementById("button-text2"),
text2Enabled: document.getElementById("text2-enabled"),
fontSize2: document.getElementById("font-size2"),
text2X: document.getElementById("text2-x"),
text2Y: document.getElementById("text2-y"),
text2ColorType: document.getElementById("text2-color-type"),
text2Color: document.getElementById("text2-color"),
text2GradientColor1: document.getElementById("text2-gradient-color1"),
text2GradientColor2: document.getElementById("text2-gradient-color2"),
text2GradientAngle: document.getElementById("text2-gradient-angle"),
text2Outline: document.getElementById("text2-outline"),
outline2Color: document.getElementById("outline2-color"),
fontFamily2: document.getElementById("font-family2"),
fontBold2: document.getElementById("font-bold2"),
fontItalic2: document.getElementById("font-italic2"),
bgType: document.getElementById("bg-type"),
bgColor: document.getElementById("bg-color"),
gradientColor1: document.getElementById("gradient-color1"),
gradientColor2: document.getElementById("gradient-color2"),
gradientAngle: document.getElementById("gradient-angle"),
textureType: document.getElementById("texture-type"),
textureColor1: document.getElementById("texture-color1"),
textureColor2: document.getElementById("texture-color2"),
textureScale: document.getElementById("texture-scale"),
borderWidth: document.getElementById("border-width"),
borderColor: document.getElementById("border-color"),
borderStyle: document.getElementById("border-style"),
};
// Update value displays
const updateValueDisplay = (id, value) => {
const display = document.getElementById(id + "-value");
if (display) display.textContent = value;
};
// Show/hide controls based on background type
controls.bgType.addEventListener("change", () => {
const solidControls = document.getElementById("solid-controls");
const gradientControls = document.getElementById("gradient-controls");
const textureControls = document.getElementById("texture-controls");
solidControls.style.display = "none";
gradientControls.style.display = "none";
textureControls.style.display = "none";
if (controls.bgType.value === "solid") {
solidControls.style.display = "block";
} else if (controls.bgType.value === "gradient") {
gradientControls.style.display = "block";
} else if (controls.bgType.value === "texture") {
textureControls.style.display = "block";
}
drawButton();
});
// Show/hide text color controls
controls.textColorType.addEventListener("change", () => {
const textSolidColor = document.getElementById("text-solid-color");
const textGradientColor = document.getElementById("text-gradient-color");
if (controls.textColorType.value === "solid") {
textSolidColor.style.display = "block";
textGradientColor.style.display = "none";
} else {
textSolidColor.style.display = "none";
textGradientColor.style.display = "block";
}
drawButton();
});
controls.text2ColorType.addEventListener("change", () => {
const text2SolidColor = document.getElementById("text2-solid-color");
const text2GradientColor = document.getElementById("text2-gradient-color");
if (controls.text2ColorType.value === "solid") {
text2SolidColor.style.display = "block";
text2GradientColor.style.display = "none";
} else {
text2SolidColor.style.display = "none";
text2GradientColor.style.display = "block";
}
drawButton();
});
// Show/hide outline color
controls.textOutline.addEventListener("change", () => {
controls.outlineColor.style.display = controls.textOutline.checked
? "block"
: "none";
drawButton();
});
controls.text2Outline.addEventListener("change", () => {
controls.outline2Color.style.display = controls.text2Outline.checked
? "block"
: "none";
drawButton();
});
// Draw texture patterns
function drawTexture(type, color1, color2, scale) {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = 88;
tempCanvas.height = 31;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.fillStyle = color1;
tempCtx.fillRect(0, 0, 88, 31);
tempCtx.fillStyle = color2;
const size = Math.max(2, Math.floor(scale / 10));
switch (type) {
case "dots":
for (let y = 0; y < 31; y += size * 2) {
for (let x = 0; x < 88; x += size * 2) {
tempCtx.beginPath();
tempCtx.arc(x, y, size / 2, 0, Math.PI * 2);
tempCtx.fill();
}
}
break;
case "grid":
for (let x = 0; x < 88; x += size) {
tempCtx.fillRect(x, 0, 1, 31);
}
for (let y = 0; y < 31; y += size) {
tempCtx.fillRect(0, y, 88, 1);
}
break;
case "diagonal":
for (let i = -31; i < 88; i += size) {
tempCtx.fillRect(i, 0, 2, 31);
tempCtx.save();
tempCtx.translate(i + 1, 0);
tempCtx.rotate(Math.PI / 4);
tempCtx.fillRect(0, 0, 2, 100);
tempCtx.restore();
}
break;
case "checkerboard":
for (let y = 0; y < 31; y += size) {
for (let x = 0; x < 88; x += size) {
if ((x / size + y / size) % 2 === 0) {
tempCtx.fillRect(x, y, size, size);
}
}
}
break;
case "noise":
for (let y = 0; y < 31; y++) {
for (let x = 0; x < 88; x++) {
if (Math.random() > 0.5) {
tempCtx.fillRect(x, y, 1, 1);
}
}
}
break;
case "stars":
for (let i = 0; i < scale; i++) {
const x = Math.floor(Math.random() * 88);
const y = Math.floor(Math.random() * 31);
tempCtx.fillRect(x, y, 1, 1);
tempCtx.fillRect(x - 1, y, 1, 1);
tempCtx.fillRect(x + 1, y, 1, 1);
tempCtx.fillRect(x, y - 1, 1, 1);
tempCtx.fillRect(x, y + 1, 1, 1);
}
break;
}
return tempCanvas;
}
// Helper function to draw to a single context
function drawToContext(context) {
context.clearRect(0, 0, 88, 31);
// Draw background
if (controls.bgType.value === "solid") {
context.fillStyle = controls.bgColor.value;
context.fillRect(0, 0, 88, 31);
} else if (controls.bgType.value === "gradient") {
const angle = parseFloat(controls.gradientAngle.value) * (Math.PI / 180);
const x1 = 44 + Math.cos(angle) * 44;
const y1 = 15.5 + Math.sin(angle) * 15.5;
const x2 = 44 - Math.cos(angle) * 44;
const y2 = 15.5 - Math.sin(angle) * 15.5;
const gradient = context.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, controls.gradientColor1.value);
gradient.addColorStop(1, controls.gradientColor2.value);
context.fillStyle = gradient;
context.fillRect(0, 0, 88, 31);
} else if (controls.bgType.value === "texture") {
const texture = drawTexture(
controls.textureType.value,
controls.textureColor1.value,
controls.textureColor2.value,
parseFloat(controls.textureScale.value),
);
context.drawImage(texture, 0, 0);
}
// Draw border
const borderWidth = parseFloat(controls.borderWidth.value);
if (borderWidth > 0) {
const style = controls.borderStyle.value;
if (style === "solid") {
context.strokeStyle = controls.borderColor.value;
context.lineWidth = borderWidth;
context.strokeRect(
borderWidth / 2,
borderWidth / 2,
88 - borderWidth,
31 - borderWidth,
);
} else if (style === "inset" || style === "outset") {
const light = style === "outset";
context.strokeStyle = light ? "#ffffff" : "#000000";
context.lineWidth = borderWidth;
context.beginPath();
context.moveTo(0, 31);
context.lineTo(0, 0);
context.lineTo(88, 0);
context.stroke();
context.strokeStyle = light ? "#000000" : "#ffffff";
context.beginPath();
context.moveTo(88, 0);
context.lineTo(88, 31);
context.lineTo(0, 31);
context.stroke();
} else if (style === "ridge") {
context.strokeStyle = "#ffffff";
context.lineWidth = borderWidth / 2;
context.strokeRect(
borderWidth / 4,
borderWidth / 4,
88 - borderWidth / 2,
31 - borderWidth / 2,
);
context.strokeStyle = "#000000";
context.strokeRect(
(borderWidth * 3) / 4,
(borderWidth * 3) / 4,
88 - borderWidth * 1.5,
31 - borderWidth * 1.5,
);
}
}
// Draw text line 1
const text = controls.text.value;
if (text && controls.textEnabled.checked) {
const fontSize = parseFloat(controls.fontSize.value);
const fontWeight = controls.fontBold.checked ? "bold" : "normal";
const fontStyle = controls.fontItalic.checked ? "italic" : "normal";
const fontFamily = controls.fontFamily.value;
context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
context.textAlign = "center";
context.textBaseline = "middle";
const x = (parseFloat(controls.textX.value) / 100) * 88;
const y = (parseFloat(controls.textY.value) / 100) * 31;
if (controls.textOutline.checked) {
context.strokeStyle = controls.outlineColor.value;
context.lineWidth = 2;
context.strokeText(text, x, y);
}
// Apply text color or gradient
if (controls.textColorType.value === "solid") {
context.fillStyle = controls.textColor.value;
} else {
const angle =
parseFloat(controls.textGradientAngle.value) * (Math.PI / 180);
const textWidth = context.measureText(text).width;
const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2;
const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
const textGradient = context.createLinearGradient(x1, y1, x2, y2);
textGradient.addColorStop(0, controls.textGradientColor1.value);
textGradient.addColorStop(1, controls.textGradientColor2.value);
context.fillStyle = textGradient;
}
context.fillText(text, x, y);
}
// Draw text line 2
const text2 = controls.text2.value;
if (text2 && controls.text2Enabled.checked) {
const fontSize2 = parseFloat(controls.fontSize2.value);
const fontWeight2 = controls.fontBold2.checked ? "bold" : "normal";
const fontStyle2 = controls.fontItalic2.checked ? "italic" : "normal";
const fontFamily2 = controls.fontFamily2.value;
context.font = `${fontStyle2} ${fontWeight2} ${fontSize2}px "${fontFamily2}"`;
context.textAlign = "center";
context.textBaseline = "middle";
const x2 = (parseFloat(controls.text2X.value) / 100) * 88;
const y2 = (parseFloat(controls.text2Y.value) / 100) * 31;
if (controls.text2Outline.checked) {
context.strokeStyle = controls.outline2Color.value;
context.lineWidth = 2;
context.strokeText(text2, x2, y2);
}
// Apply text color or gradient
if (controls.text2ColorType.value === "solid") {
context.fillStyle = controls.text2Color.value;
} else {
const angle2 =
parseFloat(controls.text2GradientAngle.value) * (Math.PI / 180);
const text2Width = context.measureText(text2).width;
const x1_2 = x2 - text2Width / 2 + (Math.cos(angle2) * text2Width) / 2;
const y1_2 = y2 - fontSize2 / 2 + (Math.sin(angle2) * fontSize2) / 2;
const x2_2 = x2 + text2Width / 2 - (Math.cos(angle2) * text2Width) / 2;
const y2_2 = y2 + fontSize2 / 2 - (Math.sin(angle2) * fontSize2) / 2;
const text2Gradient = context.createLinearGradient(
x1_2,
y1_2,
x2_2,
y2_2,
);
text2Gradient.addColorStop(0, controls.text2GradientColor1.value);
text2Gradient.addColorStop(1, controls.text2GradientColor2.value);
context.fillStyle = text2Gradient;
}
context.fillText(text2, x2, y2);
}
}
// Main draw function
function drawButton() {
drawToContext(ctx);
}
// Download function
document.getElementById("download-button").addEventListener("click", () => {
const link = document.createElement("a");
link.download = "button-88x31.png";
link.href = canvas.toDataURL();
link.click();
});
// Preset buttons
document.getElementById("preset-random").addEventListener("click", () => {
const randomColor = () =>
"#" +
Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, "0");
// Random background
controls.bgType.value = ["solid", "gradient", "texture"][
Math.floor(Math.random() * 3)
];
controls.bgColor.value = randomColor();
controls.gradientColor1.value = randomColor();
controls.gradientColor2.value = randomColor();
controls.gradientAngle.value = Math.floor(Math.random() * 360);
controls.textureColor1.value = randomColor();
controls.textureColor2.value = randomColor();
controls.textureType.value = [
"dots",
"grid",
"diagonal",
"checkerboard",
"noise",
"stars",
][Math.floor(Math.random() * 6)];
// Random text 1 color (50% chance of gradient)
controls.textColorType.value = Math.random() > 0.5 ? "gradient" : "solid";
controls.textColor.value = randomColor();
controls.textGradientColor1.value = randomColor();
controls.textGradientColor2.value = randomColor();
controls.textGradientAngle.value = Math.floor(Math.random() * 360);
// Random text 2 color (50% chance of gradient)
controls.text2ColorType.value = Math.random() > 0.5 ? "gradient" : "solid";
controls.text2Color.value = randomColor();
controls.text2GradientColor1.value = randomColor();
controls.text2GradientColor2.value = randomColor();
controls.text2GradientAngle.value = Math.floor(Math.random() * 360);
// Random border
controls.borderColor.value = randomColor();
controls.borderWidth.value = Math.floor(Math.random() * 6);
controls.borderStyle.value = ["solid", "inset", "outset", "ridge"][
Math.floor(Math.random() * 4)
];
// Random text styles
controls.fontBold.checked = Math.random() > 0.5;
controls.fontItalic.checked = Math.random() > 0.5;
controls.fontBold2.checked = Math.random() > 0.5;
controls.fontItalic2.checked = Math.random() > 0.5;
// Update displays
updateValueDisplay("gradient-angle", controls.gradientAngle.value);
updateValueDisplay("text-gradient-angle", controls.textGradientAngle.value);
updateValueDisplay(
"text2-gradient-angle",
controls.text2GradientAngle.value,
);
updateValueDisplay("border-width", controls.borderWidth.value);
controls.bgType.dispatchEvent(new Event("change"));
controls.textColorType.dispatchEvent(new Event("change"));
controls.text2ColorType.dispatchEvent(new Event("change"));
drawButton();
});
document.getElementById("preset-classic").addEventListener("click", () => {
// Classic 90s web button style
controls.bgType.value = "gradient";
controls.gradientColor1.value = "#0066cc";
controls.gradientColor2.value = "#0099ff";
controls.gradientAngle.value = 90;
controls.textColorType.value = "solid";
controls.textColor.value = "#ffffff";
controls.text2ColorType.value = "solid";
controls.text2Color.value = "#ffffff";
controls.borderWidth.value = 2;
controls.borderColor.value = "#000000";
controls.borderStyle.value = "outset";
controls.fontFamily.value = "Oswald";
controls.fontFamily2.value = "Lato";
controls.fontBold.checked = true;
controls.fontBold2.checked = false;
controls.fontItalic.checked = false;
controls.fontItalic2.checked = false;
controls.text.value = "RITUAL.SH";
controls.text2.value = "FREE THE WEB";
controls.textEnabled.checked = true;
controls.text2Enabled.checked = true;
controls.fontSize.value = 12;
controls.fontSize2.value = 8;
controls.textY.value = 35;
controls.text2Y.value = 65;
updateValueDisplay("font-size", 12);
updateValueDisplay("font-size2", 8);
updateValueDisplay("gradient-angle", 90);
updateValueDisplay("text-y", 35);
updateValueDisplay("text2-y", 65);
controls.bgType.dispatchEvent(new Event("change"));
controls.textColorType.dispatchEvent(new Event("change"));
controls.text2ColorType.dispatchEvent(new Event("change"));
drawButton();
});
document.getElementById("preset-modern").addEventListener("click", () => {
// Modern cyberpunk style with gradient text
controls.bgType.value = "gradient";
controls.gradientColor1.value = "#0a0a0a";
controls.gradientColor2.value = "#1a0a2e";
controls.gradientAngle.value = 135;
controls.textColorType.value = "gradient";
controls.textGradientColor1.value = "#00ffaa";
controls.textGradientColor2.value = "#00ffff";
controls.textGradientAngle.value = 90;
controls.text2ColorType.value = "gradient";
controls.text2GradientColor1.value = "#ff00ff";
controls.text2GradientColor2.value = "#ff6600";
controls.text2GradientAngle.value = 0;
controls.borderWidth.value = 1;
controls.borderColor.value = "#00ffaa";
controls.borderStyle.value = "solid";
controls.fontFamily.value = "Roboto Mono";
controls.fontFamily2.value = "Roboto Mono";
controls.fontBold.checked = true;
controls.fontBold2.checked = false;
controls.fontItalic.checked = false;
controls.fontItalic2.checked = false;
controls.text.value = "RITUAL.SH";
controls.text2.value = "EST. 2024";
controls.textEnabled.checked = true;
controls.text2Enabled.checked = true;
controls.fontSize.value = 11;
controls.fontSize2.value = 9;
controls.textY.value = 35;
controls.text2Y.value = 65;
updateValueDisplay("font-size", 11);
updateValueDisplay("font-size2", 9);
updateValueDisplay("gradient-angle", 135);
updateValueDisplay("text-gradient-angle", 90);
updateValueDisplay("text2-gradient-angle", 0);
updateValueDisplay("text-y", 35);
updateValueDisplay("text2-y", 65);
controls.textColorType.dispatchEvent(new Event("change"));
controls.text2ColorType.dispatchEvent(new Event("change"));
controls.bgType.dispatchEvent(new Event("change"));
drawButton();
});
// Add event listeners to all controls
Object.values(controls).forEach((control) => {
if (control) {
control.addEventListener("input", drawButton);
control.addEventListener("change", drawButton);
if (control.type === "range") {
control.addEventListener("input", (e) => {
updateValueDisplay(e.target.id, e.target.value);
});
}
}
});
// Initial draw
drawButton();
})();

View file

@ -0,0 +1,503 @@
@import url(https://fonts.bunny.net/css?family=bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400);
#button-generator-app {
margin: 2rem 0;
padding: 2rem;
background: linear-gradient(
145deg,
rgba(5, 15, 30, 0.9) 0%,
rgba(10, 20, 40, 0.95) 100%
);
border-radius: 6px;
border: 1px solid rgba(0, 150, 255, 0.4);
box-shadow:
0 0 30px rgba(0, 150, 255, 0.15),
0 0 50px rgba(255, 120, 0, 0.08),
inset 0 0 60px rgba(0, 100, 200, 0.05);
position: relative;
overflow: visible;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(
90deg,
transparent,
rgba(0, 150, 255, 0.6),
rgba(255, 120, 0, 0.6),
transparent
);
}
.generator-container {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
margin-top: 1rem;
position: relative;
z-index: 1;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.preview-section {
background: linear-gradient(
135deg,
rgba(0, 100, 180, 0.15) 0%,
rgba(0, 80, 150, 0.1) 100%
);
padding: 1.5rem;
border-radius: 6px;
border: 1px solid rgba(0, 150, 255, 0.3);
display: flex;
flex-direction: column;
gap: 1rem;
position: sticky;
top: 1rem;
align-self: flex-start;
max-height: calc(100vh - 2rem);
overflow-y: auto;
box-shadow:
0 0 20px rgba(0, 150, 255, 0.1),
inset 0 0 40px rgba(0, 100, 200, 0.05);
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(
180deg,
rgba(0, 150, 255, 0.7),
rgba(255, 120, 0, 0.5)
);
border-radius: 6px 0 0 6px;
}
h3 {
margin-top: 0;
color: #0096ff;
font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
text-shadow: 0 0 15px rgba(0, 150, 255, 0.5);
font-size: 1.2rem;
}
}
.presets-container {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 2px solid;
border-image: linear-gradient(
90deg,
transparent,
rgba(0, 150, 255, 0.5),
transparent
)
1;
h3 {
margin-top: 0;
margin-bottom: 1rem;
color: rgba(100, 180, 255, 0.9);
font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
text-shadow: 0 0 10px rgba(0, 150, 255, 0.4);
font-size: 1rem;
}
}
.preview-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.preview-wrapper {
background: repeating-conic-gradient(
rgba(20, 30, 50, 0.8) 0% 25%,
rgba(10, 20, 40, 0.9) 0% 50%
)
50% / 20px 20px;
padding: 2rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
border: 1px solid rgba(0, 150, 255, 0.2);
}
#button-canvas {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
border: 1px solid rgba(0, 150, 255, 0.4);
box-shadow: 0 0 15px rgba(0, 150, 255, 0.2);
}
.controls-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.control-group {
background: linear-gradient(
135deg,
rgba(0, 100, 180, 0.15) 0%,
rgba(0, 80, 150, 0.1) 100%
);
padding: 1.5rem;
border-radius: 6px;
border: 1px solid rgba(0, 150, 255, 0.3);
box-shadow:
0 0 20px rgba(0, 150, 255, 0.1),
inset 0 0 40px rgba(0, 100, 200, 0.05);
position: relative;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 100%;
background: linear-gradient(
180deg,
rgba(0, 150, 255, 0.7),
rgba(255, 120, 0, 0.5)
);
opacity: 0;
transition: opacity 0.3s ease;
}
&:hover::before {
opacity: 1;
}
h3 {
margin-top: 0;
margin-bottom: 1rem;
}
label {
display: block;
margin-top: 0.75rem;
margin-bottom: 0.25rem;
font-weight: 500;
font-size: 0.85rem;
color: rgba(100, 180, 255, 0.8);
text-transform: uppercase;
letter-spacing: 0.05em;
}
input[type="text"],
select {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 150, 255, 0.4);
border-radius: 4px;
font-size: 0.9rem;
background: rgba(5, 15, 30, 0.7);
color: rgba(200, 220, 255, 0.95);
transition: all 0.3s ease;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
&:focus {
outline: none;
border-color: rgba(0, 150, 255, 0.7);
box-shadow:
0 0 15px rgba(0, 150, 255, 0.3),
inset 0 2px 8px rgba(0, 0, 0, 0.4);
background: rgba(5, 15, 30, 0.9);
}
}
select {
cursor: pointer;
option {
background: rgba(5, 15, 30, 0.95);
color: rgba(200, 220, 255, 0.95);
}
}
// Font preview in dropdowns
select#font-family,
select#font-family2 {
option[value="Lato"] {
font-family: "Lato", sans-serif;
}
option[value="Roboto"] {
font-family: "Roboto", sans-serif;
}
option[value="Open Sans"] {
font-family: "Open Sans", sans-serif;
}
option[value="Montserrat"] {
font-family: "Montserrat", sans-serif;
}
option[value="Oswald"] {
font-family: "Oswald", sans-serif;
}
option[value="Bebas Neue"] {
font-family: "Bebas Neue", display;
}
option[value="Roboto Mono"] {
font-family: "Roboto Mono", monospace;
}
option[value="VT323"] {
font-family: "VT323", monospace;
}
option[value="Press Start 2P"] {
font-family: "Press Start 2P", display;
}
}
input[type="range"] {
width: 100%;
margin-top: 0.25rem;
-webkit-appearance: none;
appearance: none;
background: rgba(0, 100, 180, 0.2);
border-radius: 4px;
height: 6px;
outline: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #0096ff, #66ccff);
cursor: pointer;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
border: 2px solid rgba(0, 150, 255, 0.8);
}
&::-moz-range-thumb {
width: 18px;
height: 18px;
background: linear-gradient(135deg, #0096ff, #66ccff);
cursor: pointer;
border-radius: 50%;
box-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
border: 2px solid rgba(0, 150, 255, 0.8);
}
}
input[type="color"] {
width: 100%;
height: 50px;
border: 1px solid rgba(0, 150, 255, 0.4);
border-radius: 4px;
cursor: pointer;
background: rgba(5, 15, 30, 0.7);
padding: 4px;
transition: all 0.3s ease;
&:hover {
border-color: rgba(0, 150, 255, 0.7);
box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
}
}
input[type="checkbox"] {
width: auto;
margin-left: 0.5rem;
cursor: pointer;
accent-color: #0096ff;
}
}
.control-group-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
margin: 0;
padding: 0.5rem 0;
border-bottom: 2px solid;
border-image: linear-gradient(
90deg,
transparent,
rgba(0, 150, 255, 0.5),
transparent
)
1;
margin-bottom: 1rem;
color: rgba(100, 180, 255, 0.9);
font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
transition: all 0.3s ease;
&:hover {
color: #66ccff;
text-shadow: 0 0 10px rgba(0, 150, 255, 0.5);
}
}
.toggle-icon {
font-size: 1.5rem;
font-weight: bold;
transition: transform 0.3s ease;
color: rgba(0, 150, 255, 0.8);
.control-group.collapsed & {
transform: rotate(180deg);
}
}
.control-group-content {
overflow: hidden;
transition:
max-height 0.3s ease,
opacity 0.3s ease;
.control-group.collapsed & {
max-height: 0;
opacity: 0;
pointer-events: none;
}
}
.checkbox-row {
display: flex;
gap: 1.5rem;
margin-top: 0.75rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
font-size: 0.85rem;
cursor: pointer;
margin: 0;
color: rgba(100, 180, 255, 0.8);
text-transform: uppercase;
letter-spacing: 0.05em;
transition: color 0.3s ease;
&:hover {
color: #66ccff;
}
input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
span {
user-select: none;
}
}
.btn-primary,
.btn-secondary {
width: 100%;
padding: 0.75rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
border: 1px solid rgba(0, 150, 255, 0.5);
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.08em;
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(0, 150, 255, 0.3),
transparent
);
transition: left 0.5s ease;
}
&:hover::before {
left: 100%;
}
}
.btn-primary {
background: linear-gradient(
135deg,
rgba(0, 120, 200, 0.3) 0%,
rgba(0, 100, 180, 0.2) 100%
);
color: #66ccff;
&:hover {
background: linear-gradient(
135deg,
rgba(0, 150, 255, 0.4) 0%,
rgba(255, 100, 0, 0.3) 100%
);
border-color: rgba(0, 150, 255, 0.8);
box-shadow:
0 0 20px rgba(0, 150, 255, 0.4),
0 0 30px rgba(255, 120, 0, 0.2);
color: #fff;
text-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
}
}
.btn-secondary {
background: linear-gradient(
135deg,
rgba(0, 100, 180, 0.2) 0%,
rgba(0, 80, 150, 0.15) 100%
);
color: #66ccff;
margin-top: 0.5rem;
&:hover {
background: linear-gradient(
135deg,
rgba(0, 120, 200, 0.3) 0%,
rgba(255, 100, 0, 0.2) 100%
);
border-color: rgba(0, 150, 255, 0.6);
transform: translateY(-2px);
box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
color: #fff;
text-shadow: 0 0 8px rgba(0, 150, 255, 0.5);
}
&:active {
transform: translateY(0);
}
}
}

View file

@ -244,6 +244,7 @@
inset 0 1px 2px rgba(255, 255, 255, 0.1); inset 0 1px 2px rgba(255, 255, 255, 0.1);
border-radius: 1em; border-radius: 1em;
cursor: pointer; cursor: pointer;
position: relative;
&::after { &::after {
content: "Interests and Tools"; content: "Interests and Tools";
@ -267,7 +268,7 @@
@include media-down(lg) { @include media-down(lg) {
margin-left: -0.5em; margin-left: -0.5em;
opacity: 1; opacity: 1;
bottom: 0; bottom: -40px;
font-size: 20px; font-size: 20px;
} }
} }

View file

@ -84,7 +84,7 @@
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
z-index: 1; z-index: 10;
} }
// Portal Header with portals on either side // Portal Header with portals on either side
@ -97,7 +97,7 @@
position: relative; position: relative;
.portal-title { .portal-title {
font-size: 3.5rem; font-size: 2.5rem;
font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif; font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
@ -108,11 +108,13 @@
0 0 40px rgba(255, 255, 255, 0.4), 0 0 40px rgba(255, 255, 255, 0.4),
0 2px 4px rgba(0, 0, 0, 0.3); 0 2px 4px rgba(0, 0, 0, 0.3);
margin: 0; margin: 0;
margin-top: 50px;
position: relative; position: relative;
z-index: 2; z-index: 2;
@include media-up(md) { @include media-up(md) {
font-size: 4rem; font-size: 4rem;
margin-top: 0;
} }
} }
@ -192,7 +194,7 @@
left: 5%; left: 5%;
bottom: 5%; bottom: 5%;
width: 150px; width: 150px;
z-index: -1; z-index: 1;
.heart-icon { .heart-icon {
font-size: 2em; font-size: 2em;
@ -257,6 +259,7 @@
.portal-sign { .portal-sign {
position: relative; position: relative;
padding: 3rem 2rem; padding: 3rem 2rem;
z-index: 10;
border: 10px solid black; border: 10px solid black;
background: white; background: white;
@ -579,7 +582,8 @@
inset 0 0 100px rgba(0, 100, 200, 0.05), inset 0 0 100px rgba(0, 100, 200, 0.05),
0 8px 32px rgba(0, 0, 0, 0.4); 0 8px 32px rgba(0, 0, 0, 0.4);
position: relative; position: relative;
overflow: hidden; overflow: visible;
z-index: 10;
// Subtle tech panel grid pattern // Subtle tech panel grid pattern
&::before { &::before {

View file

@ -26,6 +26,7 @@
@import "pages/blog"; @import "pages/blog";
@import "pages/media"; @import "pages/media";
@import "pages/resources"; @import "pages/resources";
@import "pages/button-generator";
@import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400); @import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400);

View file

@ -9,6 +9,8 @@ draft: true
- ❄️ It's been pretty cold and we've had the lightest sprinkling of snow. - ❄️ It's been pretty cold and we've had the lightest sprinkling of snow.
- 📱 I did a mobile pass on some of the pages on this website, it works a bit better on phones now. - 📱 I did a mobile pass on some of the pages on this website, it works a bit better on phones now.
- 🟠 Added the [resources](/resources) section with a script to pull weekly last.fm stats (see example output below!)
- 🆒 Started working on an [88x31 button creator](/resources/button-generator/), it's got a decent amount of functionality so far.
## Links I Found Interesting ## Links I Found Interesting

View file

@ -0,0 +1,23 @@
---
title: "88x31 Button Creator"
date: 2026-01-08
description: "Make custom 88x31 pixel buttons with text, colors, gradients, and textures"
icon: "button"
demo_url: ""
source_url: ""
draft: false
---
Welcome to my 88x31 button creator, this is a pretty rough and ready implementation so it could be buggy, please let me know if you find any issues.
Currently this only supports static images and exports as png due to the basic `canvas` tag limitations. I have approximate plans for how to make this export gifs and potentially make animated buttons, please look forward to it.
Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x31) and everyone who made the buttons that appear there. You should check it out if you need inspiration for your button!
{{< button-generator >}}
---
### Changelog
- 08/01/2025 - Initial release.

View file

@ -33,5 +33,11 @@
<main role="main">{{ block "main" . }}{{ end }}</main> <main role="main">{{ block "main" . }}{{ end }}</main>
{{ block "footer" . }}{{ partial "site-footer.html" . }}{{ end }} {{ block {{ block "footer" . }}{{ partial "site-footer.html" . }}{{ end }} {{ block
"scripts" . }}{{ partial "site-scripts.html" . }}{{ end }} "scripts" . }}{{ partial "site-scripts.html" . }}{{ end }}
<!-- Button Generator - only load if page content contains button-generator shortcode -->
{{ if or (findRE "{{<\\s*button-generator" .RawContent) (findRE "{{%\\s*button-generator" .RawContent) }}
{{ $buttonGenerator := resources.Get "js/button-generator.js" | resources.Minify | resources.Fingerprint }}
<script src="{{ $buttonGenerator.RelPermalink }}" integrity="{{ $buttonGenerator.Data.Integrity }}"></script>
{{ end }}
</body> </body>
</html> </html>

View file

@ -7,7 +7,7 @@
{{ $filtered := slice }} {{ $filtered := slice }}
{{ range $remaining }} {{ range $remaining }}
{{ $path := .RelPermalink }} {{ $path := .RelPermalink }}
{{ if and (not (strings.Contains $path "/terminal.js")) (not (strings.Contains $path "/init.js")) }} {{ if and (not (strings.Contains $path "/terminal.js")) (not (strings.Contains $path "/init.js")) (not (strings.Contains $path "/button-generator.js")) }}
{{ $filtered = $filtered | append . }} {{ $filtered = $filtered | append . }}
{{ end }} {{ end }}
{{ end }} {{ end }}

View file

@ -48,5 +48,8 @@
{{ end }} {{ end }}
</nav> </nav>
</article> </article>
<div class="background-cube">
{{ partial "elements/companion-cube.html" . }}
</div>
</div> </div>
{{ end }} {{ end }}

View file

@ -0,0 +1,315 @@
<div id="button-generator-app">
<div class="generator-container">
<div class="preview-section">
<h3>Preview</h3>
<div class="preview-container">
<div class="preview-wrapper">
<canvas id="button-canvas" width="88" height="31"></canvas>
</div>
</div>
<button id="download-button" class="btn-primary">Download Button</button>
<div class="presets-container">
<h3>Presets</h3>
<button id="preset-random" class="btn-secondary">Random Button</button>
<button id="preset-classic" class="btn-secondary">Classic Style</button>
<button id="preset-modern" class="btn-secondary">Modern Style</button>
</div>
</div>
<div class="controls-section">
<div class="control-group">
<h3 class="control-group-header">
<span>Text Line 1</span>
<span class="toggle-icon"></span>
</h3>
<div class="control-group-content">
<label for="button-text">Text</label>
<input
type="text"
id="button-text"
value="RITUAL.SH"
maxlength="20"
/>
<label class="checkbox-label">
<input type="checkbox" id="text-enabled" checked />
<span>Enable Text Line 1</span>
</label>
<label for="font-size"
>Font Size: <span id="font-size-value">14</span>px</label
>
<input type="range" id="font-size" min="6" max="24" value="14" />
<label for="text-x"
>Horizontal Position: <span id="text-x-value">50</span>%</label
>
<input type="range" id="text-x" min="0" max="100" value="50" />
<label for="text-y"
>Vertical Position: <span id="text-y-value">35</span>%</label
>
<input type="range" id="text-y" min="0" max="100" value="35" />
<label for="text-color-type">Text Color Type</label>
<select id="text-color-type">
<option value="solid">Solid Color</option>
<option value="gradient">Gradient</option>
</select>
<div id="text-solid-color">
<label for="text-color">Text Color</label>
<input type="color" id="text-color" value="#ffffff" />
</div>
<div id="text-gradient-color" style="display: none">
<label for="text-gradient-color1">Gradient Color 1</label>
<input type="color" id="text-gradient-color1" value="#ffffff" />
<label for="text-gradient-color2">Gradient Color 2</label>
<input type="color" id="text-gradient-color2" value="#00ffff" />
<label for="text-gradient-angle"
>Gradient Angle:
<span id="text-gradient-angle-value">0</span>°</label
>
<input
type="range"
id="text-gradient-angle"
min="0"
max="360"
value="0"
/>
</div>
<label class="checkbox-label">
<input type="checkbox" id="text-outline" />
<span>Outline</span>
</label>
<input
type="color"
id="outline-color"
value="#000000"
style="display: none"
/>
<label for="font-family">Font</label>
<select id="font-family">
<option value="Lato">Lato</option>
<option value="Roboto">Roboto</option>
<option value="Open Sans">Open Sans</option>
<option value="Montserrat">Montserrat</option>
<option value="Oswald">Oswald</option>
<option value="Bebas Neue">Bebas Neue</option>
<option value="Roboto Mono">Roboto Mono</option>
<option value="VT323">VT323</option>
<option value="Press Start 2P">Press Start 2P</option>
<option value="DSEG7-Classic">DSEG7</option>
</select>
<div class="checkbox-row">
<label class="checkbox-label">
<input type="checkbox" id="font-bold" />
<span>Bold</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="font-italic" />
<span>Italic</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<h3 class="control-group-header">
<span>Text Line 2</span>
<span class="toggle-icon"></span>
</h3>
<div class="control-group-content">
<label for="button-text2">Text</label>
<input type="text" id="button-text2" value="" maxlength="20" />
<label class="checkbox-label">
<input type="checkbox" id="text2-enabled" />
<span>Enable Text Line 2</span>
</label>
<label for="font-size2"
>Font Size: <span id="font-size2-value">12</span>px</label
>
<input type="range" id="font-size2" min="6" max="24" value="12" />
<label for="text2-x"
>Horizontal Position: <span id="text2-x-value">50</span>%</label
>
<input type="range" id="text2-x" min="0" max="100" value="50" />
<label for="text2-y"
>Vertical Position: <span id="text2-y-value">65</span>%</label
>
<input type="range" id="text2-y" min="0" max="100" value="65" />
<label for="text2-color-type">Text Color Type</label>
<select id="text2-color-type">
<option value="solid">Solid Color</option>
<option value="gradient">Gradient</option>
</select>
<div id="text2-solid-color">
<label for="text2-color">Text Color</label>
<input type="color" id="text2-color" value="#ffffff" />
</div>
<div id="text2-gradient-color" style="display: none">
<label for="text2-gradient-color1">Gradient Color 1</label>
<input type="color" id="text2-gradient-color1" value="#ffffff" />
<label for="text2-gradient-color2">Gradient Color 2</label>
<input type="color" id="text2-gradient-color2" value="#00ffff" />
<label for="text2-gradient-angle"
>Gradient Angle:
<span id="text2-gradient-angle-value">0</span>°</label
>
<input
type="range"
id="text2-gradient-angle"
min="0"
max="360"
value="0"
/>
</div>
<label class="checkbox-label">
<input type="checkbox" id="text2-outline" />
<span>Outline</span>
</label>
<input
type="color"
id="outline2-color"
value="#000000"
style="display: none"
/>
<label for="font-family2">Font</label>
<select id="font-family2">
<option value="Lato">Lato</option>
<option value="Roboto">Roboto</option>
<option value="Open Sans">Open Sans</option>
<option value="Montserrat">Montserrat</option>
<option value="Oswald">Oswald</option>
<option value="Bebas Neue">Bebas Neue</option>
<option value="Roboto Mono">Roboto Mono</option>
<option value="VT323">VT323</option>
<option value="Press Start 2P">Press Start 2P</option>
<option value="DSEG7-Classic">DSEG7</option>
</select>
<div class="checkbox-row">
<label class="checkbox-label">
<input type="checkbox" id="font-bold2" />
<span>Bold</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="font-italic2" />
<span>Italic</span>
</label>
</div>
</div>
</div>
<div class="control-group">
<h3 class="control-group-header">
<span>Background</span>
<span class="toggle-icon"></span>
</h3>
<div class="control-group-content">
<label for="bg-type">Background Type</label>
<select id="bg-type">
<option value="solid">Solid Color</option>
<option value="gradient">Gradient</option>
<option value="texture">Texture</option>
</select>
<div id="solid-controls">
<label for="bg-color">Background Color</label>
<input type="color" id="bg-color" value="#0066cc" />
</div>
<div id="gradient-controls" style="display: none">
<label for="gradient-color1">Color 1</label>
<input type="color" id="gradient-color1" value="#0066cc" />
<label for="gradient-color2">Color 2</label>
<input type="color" id="gradient-color2" value="#00ccff" />
<label for="gradient-angle"
>Angle: <span id="gradient-angle-value">90</span>°</label
>
<input
type="range"
id="gradient-angle"
min="0"
max="360"
value="90"
/>
</div>
<div id="texture-controls" style="display: none">
<label for="texture-type">Texture Pattern</label>
<select id="texture-type">
<option value="dots">Dots</option>
<option value="grid">Grid</option>
<option value="diagonal">Diagonal Lines</option>
<option value="checkerboard">Checkerboard</option>
<option value="noise">Noise</option>
<option value="stars">Stars</option>
</select>
<label for="texture-color1">Base Color</label>
<input type="color" id="texture-color1" value="#0066cc" />
<label for="texture-color2">Pattern Color</label>
<input type="color" id="texture-color2" value="#0099ff" />
<label for="texture-scale"
>Pattern Scale: <span id="texture-scale-value">50</span>%</label
>
<input
type="range"
id="texture-scale"
min="10"
max="100"
value="50"
/>
</div>
</div>
</div>
<div class="control-group">
<h3 class="control-group-header">
<span>Border</span>
<span class="toggle-icon"></span>
</h3>
<div class="control-group-content">
<label for="border-width"
>Border Width: <span id="border-width-value">2</span>px</label
>
<input type="range" id="border-width" min="0" max="5" value="2" />
<label for="border-color">Border Color</label>
<input type="color" id="border-color" value="#000000" />
<label for="border-style">Border Style</label>
<select id="border-style">
<option value="solid">Solid</option>
<option value="inset">Inset (3D)</option>
<option value="outset">Outset (3D)</option>
<option value="ridge">Ridge</option>
</select>
</div>
</div>
</div>
</div>
</div>