feat: adjust max and step values for sharpen, edgeThreshold, and scanlines controls.

This commit is contained in:
syntaxbullet
2026-02-10 15:21:34 +01:00
parent 73a6681ceb
commit a9d2c43bfd
16 changed files with 294 additions and 104 deletions

15
Caddyfile Normal file
View File

@@ -0,0 +1,15 @@
{
email me@syntaxbullet.com
}
yourdomain.com {
reverse_proxy web:4321
# Enable compression
encode zstd gzip
# Robust logging
log {
output file /var/log/caddy/access.log
}
}

View File

@@ -1,10 +1,17 @@
# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage
FROM node:22-alpine AS runtime
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
ENV HOST=0.0.0.0
ENV PORT=4321

View File

@@ -1,8 +1,27 @@
services:
web:
build: .
ports:
- "4321:4321"
restart: always
environment:
- PORT=4321
- HOST=0.0.0.0
caddy:
image: caddy:latest
restart: always
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
depends_on:
- web
volumes:
caddy_data:
caddy_config:
caddy_logs:

10
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/node": "^9.5.2",
"@lucide/astro": "^0.563.0",
"astro": "^5.17.1",
"gifuct-js": "^2.1.2",
"pngjs": "^7.0.0",
@@ -1181,6 +1182,15 @@
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@lucide/astro": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/@lucide/astro/-/astro-0.563.0.tgz",
"integrity": "sha512-X9fNJvRR6pLJfkIEAFQkizWaNVvcduunJoFyR3fwPu30Y6jOu5S9k4k7HTSk3ZrEfqK2eFEqrBqqWH4fwSNKCg==",
"license": "ISC",
"peerDependencies": {
"astro": "^4 || ^5"
}
},
"node_modules/@oslojs/encoding": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",

View File

@@ -17,6 +17,7 @@
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/node": "^9.5.2",
"@lucide/astro": "^0.563.0",
"astro": "^5.17.1",
"gifuct-js": "^2.1.2",
"pngjs": "^7.0.0",

4
public/favicon.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="black"/>
<text x="50" y="70" font-family="monospace" font-size="70" fill="white" text-anchor="middle" font-weight="900">S</text>
</svg>

After

Width:  |  Height:  |  Size: 240 B

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://syntaxbullet.com/sitemap-index.xml

View File

@@ -3,24 +3,13 @@ import TuiSlider from "./TuiSlider.astro";
import TuiSegment from "./TuiSegment.astro";
import TuiToggle from "./TuiToggle.astro";
import TuiButton from "./TuiButton.astro";
import { ChevronDown } from "@lucide/astro";
---
<footer id="tui-controls" class="control-panel">
<div class="mobile-controls-header">
<span class="mobile-controls-title">CONTROLS</span>
<svg
class="mobile-toggle-icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<ChevronDown class="mobile-toggle-icon" size={24} />
</div>
<div class="control-panel-inner">
@@ -569,6 +558,10 @@ import TuiButton from "./TuiButton.astro";
.sliders-grid {
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
}
.shortcuts-hint {
display: none;
}
}
.tui-color-btn {

View File

@@ -1,23 +1,11 @@
---
import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
---
<aside class="sidebar">
<div class="mobile-header">
<span class="mobile-brand">SYNTAXBULLET</span>
<svg
class="mobile-toggle-icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<ChevronDown class="mobile-toggle-icon" size={24} />
</div>
<div class="sidebar-content">
<div class="brand-group">
@@ -38,25 +26,24 @@
<div class="sidebar-actions">
<a href="/" class="sidebar-link">
<span class="icon"></span> GENERATE
<span class="icon"><Zap size={20} /></span> GENERATE
</a>
<a href="/blog" class="sidebar-link">
<span class="icon">📝</span> BLOG
<span class="icon"><FileText size={20} /></span> BLOG
</a>
<a href="mailto:me@syntaxbullet.com" class="sidebar-link">
<span class="icon">✉️</span> CONTACT
<span class="icon"><Mail size={20} /></span> CONTACT
</a>
</div>
<div class="sidebar-social">
<a
href="https://github.com/syntaxbullet"
href="https://git.ayau.me/syntaxbullet"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
aria-label="Git"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
@@ -65,19 +52,20 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
></path><path d="M9 18c-4.51 2-5-2.64-5-2.64"></path></svg
>
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"
></path>
<path d="M9 18c-4.51 2-5-2-7-2"></path>
</svg>
</a>
<a
href="https://linkedin.com/in/syntaxbullet"
href="https://www.linkedin.com/in/ivan-jovanovic-51b319187/"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
@@ -86,32 +74,13 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
>
<path
d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path><rect width="4" height="12" x="2" y="9"
></rect><circle cx="4" cy="4" r="2"></circle></svg
>
</a>
<a
href="https://twitter.com/syntaxbullet"
target="_blank"
rel="noopener noreferrer"
aria-label="Twitter"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
></path></svg
>
></path>
<rect width="4" height="12" x="2" y="9"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a>
</div>
</div>

View File

@@ -0,0 +1,20 @@
---
title: "The Future of Syntaxbullet"
description: "A glimpse into what's coming next for this digital garden."
pubDate: "2026-02-10"
heroImage: "/blog-placeholder-1.jpg"
---
# Welcome to my Website
This digital garden is currently sprouting.
I'm working on a series of articles that explore the intersection of **engineering, design, and artificial intelligence**.
Upcoming topics will include:
- Deep dives into the ASCII art generation techniques used on the homepage.
- Modern web performance optimization strategies.
- Thoughts on the evolving role of AI in software development.
- Case studies of successful AI-powered software projects and papers.
Stay tuned for updates. In the meantime, feel free to play with the [generator](/).

View File

@@ -1,24 +0,0 @@
---
title: 'System Initialization'
description: 'Bootstrapping the Neko ASCII Generator.'
pubDate: '2026-02-08'
heroImage: '/blog/boot.png'
---
## Initializing Core Systems...
The Neko ASCII Auto-Generator has been successfully migrated to the Astro framework.
### Features
- Real-time image processing
- CLI-inspired controls
- Dynamic font scaling
- Automatic parameter tuning based on image histogram
### Changelog v2.0
- Migrated from vanilla HTML/JS to Astro
- Added Blog module
- Improved mobile responsiveness
Running diagnostics... **OK**
Systems online.

View File

@@ -12,6 +12,10 @@ const { title, showScroll = false } = Astro.props;
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Syntaxbullet - Full Stack Engineer & Creative Technologist. Building high-performance digital experiences with a focus on engineering, design, and AI."
/>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

View File

@@ -14,8 +14,33 @@ import ControlPanel from "../components/ControlPanel.astro";
<!-- Canvas Layer -->
<div class="canvas-layer">
<div id="loading">Loading...</div>
<pre id="ascii-result">Initializing...</pre>
<pre id="ascii-result"></pre>
<canvas id="ascii-canvas"></canvas>
<!-- Landing Screen -->
<div id="landing-screen" class="landing-overlay">
<div class="landing-content">
<h1>ASCII Art Generator</h1>
<p>
Generate stunning ASCII art from images. Pull a
random image from an anime API or upload your own to
get started.
</p>
<div class="landing-buttons">
<button id="btn-start-api" class="landing-btn"
>Anime API</button
>
<button id="btn-start-upload" class="landing-btn"
>Upload Image</button
>
</div>
<p class="disclaimer">
<b>Disclaimer:</b> Images loaded via the API are not my
own and are not filtered or curated. In rare cases, they
might contain sensitive material.
</p>
</div>
</div>
</div>
<ControlPanel />
@@ -114,11 +139,36 @@ import ControlPanel from "../components/ControlPanel.astro";
}
}
// ============= Initialize UI and Load First Image =============
// ============= Initialize UI =============
ui.init();
// ============= Landing Screen Logic =============
const landingScreen = document.getElementById("landing-screen");
const btnStartApi = document.getElementById("btn-start-api");
const btnStartUpload = document.getElementById("btn-start-upload");
const fileInput = document.getElementById(
"file-upload",
) as HTMLInputElement;
const hideLanding = () => {
landingScreen?.classList.add("hidden");
};
btnStartApi?.addEventListener("click", () => {
hideLanding();
loadNewImage().then(() => {
queue.ensureFilled();
});
});
btnStartUpload?.addEventListener("click", () => {
fileInput?.click();
});
// Hide landing screen when an image is imported (manual upload)
document.addEventListener("ascii-image-imported", () => {
hideLanding();
});
</script>
<Tooltip />
</Layout>
@@ -189,6 +239,114 @@ import ControlPanel from "../components/ControlPanel.astro";
border-radius: 4px;
}
/* Landing Overlay */
.landing-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(12px);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
transition:
opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
visibility 0.6s;
}
.landing-overlay.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.landing-content {
max-width: 440px;
padding: 2.5rem;
text-align: center;
background: rgba(15, 15, 15, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.6);
animation: landing-in 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes landing-in {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.landing-content h1 {
font-size: 1.75rem;
margin-bottom: 1rem;
font-weight: 700;
letter-spacing: -0.02em;
color: #fff;
}
.landing-content p {
font-size: 0.95rem;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 2rem;
}
.landing-buttons {
display: flex;
gap: 0.75rem;
justify-content: center;
margin-bottom: 2rem;
}
.landing-btn {
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.05);
color: #fff;
cursor: pointer;
flex: 1;
}
.landing-btn:hover {
background: #fff;
color: #000;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
}
#btn-start-api {
background: #fff;
color: #000;
border: none;
}
#btn-start-api:hover {
background: #ccc;
}
.disclaimer {
font-size: 0.7rem !important;
color: rgba(255, 255, 255, 0.3) !important;
line-height: 1.4 !important;
margin-bottom: 0 !important;
text-align: left;
padding-top: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
/* Responsive */
@media (max-width: 1024px) {
.split-layout {

View File

@@ -92,7 +92,7 @@ export class AsciiExporter {
const b = pixels[i + 2];
// 1. Calculate Luma
let luma = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
// 1. Calculate Luma (skipped, using adjusted color luma below)
// 2. Apply Adjustments
// Note: For color mode, we might want to keep original color

View File

@@ -472,6 +472,8 @@ export class UIBindings {
this.updateUI();
await this.controller.generate();
this.controller.hideLoading();
// Notify that an image was successfully imported
document.dispatchEvent(new CustomEvent('ascii-image-imported'));
} catch (err) {
console.error("Import failed:", err);
this.controller.hideLoading();

View File

@@ -2,8 +2,11 @@
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020", "DOM"],
"moduleResolution": "bundler",
"lib": [
"ES2020",
"DOM"
],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
@@ -16,6 +19,11 @@
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}