Initial Release

This commit is contained in:
2026-05-24 19:52:45 -07:00
commit 8242fdbbb3
181 changed files with 10119 additions and 0 deletions
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="uuid-bba056a5-9009-46f9-846a-ccbc6d996e35" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.32 30">
<g>
<path d="M11.55,7.48c-.37-.92-1.57-1.98-2.38-1.98s-2.01,1.06-2.38,1.98c-1.75-.19-3.12-1.68-3.12-3.48S5.03.71,6.77.52c.2.49.62.92.83,1.12.28.26,1,.86,1.68.86.82-.03,1.92-1.07,2.27-1.98,1.75.2,3.11,1.68,3.11,3.48s-1.37,3.29-3.12,3.48Z" style="fill: #2bb673;"/>
<path d="M6.5,1.07c.6,1.02,1.89,1.93,2.79,1.93h.04s.02,0,.02,0c.88-.03,1.97-.95,2.5-1.92,1.33.31,2.32,1.5,2.32,2.92s-1,2.62-2.33,2.93c-.57-.99-1.74-1.93-2.67-1.93s-2.1.93-2.67,1.93c-1.33-.3-2.33-1.5-2.33-2.93s1-2.62,2.34-2.93M11.16,0c0,.68-1.17,1.97-1.85,2,0,0-.02,0-.02,0-.74,0-2.12-1.27-2.12-2C4.95,0,3.16,1.79,3.16,4s1.79,4,4,4c0-.71,1.29-2,2-2s2,1.29,2,2c2.21,0,4-1.79,4-4S13.37,0,11.16,0h0Z" style="fill: #231f20;"/>
</g>
<path d="M17.16,6c3.48,5.64-1.37,24-8,24S-2.32,11.64,1.16,6c1.18-1.9,5.76-2,8-2s6.82.1,8,2Z" style="fill: #f7941d;"/>
<g>
<path d="M12,27.79c-.37-.69-1.46-2.5-3-2.5-1.54,0-2.62,1.81-3,2.5h6Z" style="fill: #fbb040;"/>
<path d="M12.42,28.04h-6.84l.2-.37c.53-.98,1.65-2.63,3.22-2.63h0c1.56,0,2.69,1.65,3.22,2.63l.2.37ZM6.44,27.54h5.14c-.53-.88-1.43-2-2.57-2h0c-1.13,0-2.04,1.13-2.57,2Z" style="fill: #fbb040;"/>
</g>
<path d="M1.16,12.09c1-.69,3.88-2.5,7.99-2.5,4.12,0,7.01,1.81,8.01,2.5" style="fill: none; stroke: #fbb040; stroke-miterlimit: 10; stroke-width: 3px;"/>
<path d="M1.66,17.74c.94-.69,3.64-2.5,7.49-2.5,3.86,0,6.57,1.81,7.51,2.5" style="fill: none; stroke: #fbb040; stroke-miterlimit: 10; stroke-width: 2.5px;"/>
<path d="M3.84,23.5l-1.36-1.47c1.12-1.03,3.47-2.76,6.67-2.77h0c3.21,0,5.56,1.73,6.68,2.77l-1.36,1.47c-.9-.83-2.79-2.23-5.32-2.23h0c-2.52,0-4.41,1.4-5.31,2.23Z" style="fill: #fbb040;"/>
<path d="M9.16,6c5.53,0,6.27,1.01,6.3,1.05,1.85,2.99.61,11.88-2.41,17.29-1.28,2.29-2.73,3.66-3.89,3.66s-2.61-1.37-3.89-3.66C2.25,18.93,1.01,10.04,2.86,7.05c.03-.04.77-1.05,6.3-1.05M9.16,4c-2.24,0-6.82.1-8,2-3.48,5.64,1.37,24,8,24S20.64,11.64,17.16,6c-1.18-1.9-5.76-2-8-2h0Z" style="fill: #231f20;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+541
View File
@@ -0,0 +1,541 @@
:root {
--corner-thickness: 1px;
--corner-margin: -16px;
--corner-color: #606060;
--glass-tint: rgba(255, 255, 255, .075);
--glass-blur: 4px;
--color-accent: #323232;
--color-background: #000;
--color-text-default: #f0f0f0;
--color-text-hidden: #a0a0a0;
}
::-webkit-scrollbar {
width: 0;
}
body {
padding: 0;
margin: 0;
background-color: var(--color-background);
}
button,
span,
h1,
h2,
h3,
h4,
h5,
h6,
p,
a {
font-family: 'Terminus', sans-serif;
font-weight: normal;
font-size: 1em;
line-height: 1.5em;
text-decoration: none;
color: var(--color-text-default);
padding: 0;
margin: 0;
}
a:hover,
a:focus-visible {
text-decoration: underline;
}
/* Layout */
div.layout-wrapper {
display: grid;
width: 100%;
height: 100%;
}
div.layout-background {
position: fixed;
}
div.layout-foreground {
margin: auto;
width: 100%;
max-width: 1024px;
display: flex;
gap: 16px;
justify-content: center;
padding: 16px 0;
box-sizing: border-box;
}
div.layout-pane {
grid-row: 1;
grid-column: 1;
height: fit-content;
}
/* Navigation Links */
nav.section-navigation {
width: 300px;
overflow-y: scroll;
overflow-x: hidden;
max-height: calc(100vh - 64px);
padding: 8px;
box-sizing: border-box;
}
button.nav-header {
position: relative;
text-align: left;
width: 100%;
padding: 4px;
padding-right: 16px;
background: transparent;
border: none;
border-bottom: 1px solid transparent;
cursor: pointer;
}
button.nav-header.open {
border-color: var(--color-text-hidden);
}
button.nav-header img {
height: 16px;
width: 16px;
position: absolute;
right: 16px;
}
p.nav-message {
color: var(--color-text-hidden);
text-align: center;
padding: 8px;
}
a.item-nav {
width: 100%;
display: flex;
padding: 12px 8px 12px 4px;
box-sizing: border-box;
}
div.item-nav-text {
flex-basis: 100%;
}
div.item-nav-text p.item-nav-text-anchor,
div.item-nav-text p.item-nav-text-content {
font-family: 'Terminus', serif;
color: var(--color-text-default);
line-height: 1em;
font-size: 1em;
}
div.item-nav-text p.item-nav-text-content {
color: var(--color-text-hidden);
}
div.item-nav-icon {
display: grid;
}
div.item-nav-icon div.item-nav-icon-background,
div.item-nav-icon img.item-nav-icon-foreground {
grid-row: 1;
grid-column: 1;
margin: auto;
}
div.item-nav-icon div.item-nav-icon-background {
background-color: var(--color-accent);
transform: rotate(45deg);
z-index: -1;
height: 32px;
width: 32px;
}
div.item-nav-icon img.item-nav-icon-foreground {
width: 16px;
height: auto;
}
/* Navigation Chapters */
div.nav-list#chapters {
display: grid;
padding: 4px;
}
div.nav-list#chapters a.section {
font-size: small;
padding-left: 8px;
}
/* Article List */
div.item-article-count {
width: 100%;
background-color: var(--color-accent);
}
div.item-article-count p.item-article-count-text {
display: block;
width: 100%;
height: 32px;
padding: 0 8px;
line-height: 32px;
box-sizing: border-box;
color: var(--color-text-hidden);
}
div.item-article-count span.item-article-count-cursor {
color: var(--color-text-hidden);
}
a.item-article {
display: grid;
height: 80px;
}
div.item-article-layer {
width: 100%;
height: 100%;
grid-row: 1;
grid-column: 1;
}
div.item-article-meta {
padding: 8px 0;
box-sizing: border-box;
}
p.item-article-meta-title {
padding-left: 10px;
}
p.item-article-meta-description {
color: var(--color-text-hidden);
padding-left: 10px;
font-size: small;
}
div.item-article-meta-tags {
display: flex;
padding-left: 4px;
gap: 4px;
}
div.item-article-meta-tags>span {
color: var(--color-text-hidden);
padding: 0 6px;
font-size: small;
}
img.item-article-banner {
position: absolute;
right: 0;
height: 80px;
object-fit: cover;
aspect-ratio: 3 / 1;
mask-image: linear-gradient(-90deg, rgba(0, 0, 0, .5) 0%, transparent 100%);
}
/* Article Header */
article.section-article {
width: 600px;
max-height: calc(100vh - 64px);
overflow-y: scroll;
box-sizing: border-box;
}
div.article-header {
width: 100%;
height: fit-content;
display: grid;
justify-items: center;
}
div.header-chevrons {
display: flex;
justify-content: space-between;
width: 100%;
}
div.article-content:has(*) {
display: grid;
gap: 8px;
padding: 16px;
box-sizing: border-box;
}
img#header-banner {
width: 100%;
object-fit: cover;
aspect-ratio: 3 / 1;
}
h1#header-title {
word-wrap: break-word;
overflow: hidden;
margin: 16px 0 24px 0;
}
/* Article Elements */
h2.element-header {
font-size: x-large;
font-weight: bold;
border-bottom: 1px solid;
margin-bottom: 8px;
padding-bottom: 8px 0;
padding-top: 16px;
}
h3.element-subheader {
font-weight: bold;
text-decoration: underline;
padding-bottom: 8px;
padding-top: 16px;
}
p.element-paragraph {
line-height: 1.25;
text-align: justify;
}
/* Animations */
.animation-blink {
animation: kf-blink 1s infinite step-start;
}
@keyframes kf-blink {
50% {
opacity: 0;
}
}
.animation-scrollin {
box-sizing: border-box;
overflow: hidden;
text-wrap-mode: nowrap;
animation: kf-scrollin 750ms forwards linear;
}
@keyframes kf-scrollin {
0% {
width: 0%;
}
100% {
width: 100%;
}
}
.animation-fadein {
animation: kf-fadein 500ms forwards linear;
}
@keyframes kf-fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/* Effects */
.effect-glass-wrapper {
position: relative;
margin: 16px;
}
.effect-docked {
font-size: 2em;
font-weight: bold;
text-align: center;
position: relative;
padding: 0 16px;
z-index: 1;
}
.effect-docked::before {
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 0;
left: 0;
box-sizing: border-box;
background: var(--color-accent);
clip-path: polygon(12px 60%, calc(100% - 12px) 60%, 100% 100%, 0 100%);
z-index: -1;
}
div.effect-chevron-container {
align-items: end;
}
.effect-chevron-point-left,
.effect-chevron-point-right {
display: inline-block;
background-color: var(--color-accent);
text-decoration: none;
font-weight: bold;
height: 24px;
width: fit-content;
padding: 0 16px;
}
.effect-chevron-point-right {
margin-right: -4px;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 8px 50%);
}
div.effect-chevron-container>.effect-chevron-point-right:first-child {
padding-left: 8px;
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%);
}
.effect-chevron-point-left {
margin-left: -4px;
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 50%, 100% 100%, 8px 100%, 0 50%);
}
div.effect-chevron-container>.effect-chevron-point-left:last-child {
padding-right: 8px;
clip-path: polygon(8px 0, 100% 0, 100% 100%, 8px 100%, 0 50%);
}
.effect-glass-container {
background: var(--glass-tint);
backdrop-filter: blur(var(--glass-blur));
}
.effect-glass-corner {
position: absolute;
width: 24px;
height: 24px;
pointer-events: none;
z-index: 999;
}
.effect-glass-corner:nth-child(1) {
/* Top-left */
top: var(--corner-margin);
left: var(--corner-margin);
border-top: var(--corner-thickness) solid var(--corner-color);
border-left: var(--corner-thickness) solid var(--corner-color);
}
.effect-glass-corner:nth-child(2) {
/* Top-right */
top: var(--corner-margin);
right: var(--corner-margin);
border-top: var(--corner-thickness) solid var(--corner-color);
border-right: var(--corner-thickness) solid var(--corner-color);
}
.effect-glass-corner:nth-child(3) {
/* Bottom-left */
bottom: var(--corner-margin);
left: var(--corner-margin);
border-bottom: var(--corner-thickness) solid var(--corner-color);
border-left: var(--corner-thickness) solid var(--corner-color);
}
.effect-glass-corner:nth-child(4) {
/* Bottom-right */
bottom: var(--corner-margin);
right: var(--corner-margin);
border-bottom: var(--corner-thickness) solid var(--corner-color);
border-right: var(--corner-thickness) solid var(--corner-color);
}
/* Mobile */
@media only screen and (max-width: 600px) {
div.layout-foreground {
display: block;
padding: 0;
}
nav.section-navigation {
width: 100%;
max-height: unset;
}
article.section-article {
width: 100%;
max-height: unset;
box-sizing: border-box;
}
/* Make Header Slightly Smaller */
h1#header-title {
font-size: 1.5em;
}
div.header-chevrons {
display: grid;
justify-content: center;
}
div.effect-chevron-container {
text-align: center;
}
.effect-chevron-point-left,
.effect-chevron-point-right {
margin: 2px 0;
}
.effect-chevron-point-right {
margin-right: unset;
clip-path: unset;
}
div.effect-chevron-container>.effect-chevron-point-right:first-child {
padding-left: 16px;
clip-path: unset;
}
.effect-chevron-point-left {
margin-left: unset;
clip-path: unset;
}
div.effect-chevron-container>.effect-chevron-point-left:last-child {
padding-right: 16px;
clip-path: unset;
}
/* Remove Filters from Navigation */
button.nav-header[for='authors'],
button.nav-header[for='tags'],
div.nav-list#authors,
div.nav-list#tags {
display: none;
}
.effect-glass-wrapper {
margin: unset;
}
.effect-glass-corner {
display: none;
}
.effect-glass-container {
padding: 0;
background: transparent;
}
}
@@ -0,0 +1,59 @@
{
"links": [
{
"id": "/blog/",
"icon": "script:icon_top",
"name": "LATEST",
"description": "BROWSE ARTICLES"
},
{
"id": "/blog/rss.xml",
"icon": "script:icon_rss",
"name": "RSS",
"description": "FOR NERDS WITH READERS"
},
{
"id": "/",
"icon": "script:icon_cross",
"name": "EXIT",
"description": "RETURN TO HOMEPAGE"
}
],
"authors": [
{
"id": "bakonpancakz",
"icon": "script:author_bakonpancakz",
"name": "BAKONPANCAKZ",
"description": "THE BOSS"
}
],
"tags": [
{
"id": "development",
"icon": "script:tags_development",
"name": "DEVELOPMENT",
"description": "BINARY MAGIC / SPELLS"
},
{
"id": "personal",
"icon": "script:tags_personal",
"name": "PERSONAL",
"description": "PROJECTS AND TIDBITS"
}
],
"articles": [
{
"id": "setting-up-an-email-server",
"banner_main": "/public/blog/setting-up-an-email-server/banner_main.gif",
"banner_list": "/public/blog/setting-up-an-email-server/banner_list.gif",
"date": "2024-11-06T00:00:00Z",
"title": "Setting Up an Email API",
"description": "Freaking emails, how do they work?",
"author": "bakonpancakz",
"tags": [
"development",
"personal"
]
}
]
}
@@ -0,0 +1,357 @@
document.querySelector("div.layout-background").appendChild((() => {
const COLOR_PARTICLE = 0x363636
const COLOR_FOREGROUND = 0x282828
const COLOR_BACKGROUND = 0x000000
const PARTICLE_COUNT = 320
const FRAME_TIME = 1000 / 15
let gl, canvas
let planeProg, particleProg
let planeBuf, planeIdxBuf, planeOrigBuf
let particlePosBuf, particleColorBuf
let planeVerts, planeOrig, planeIdx
let particlePos, particleColor, particleData = []
let lastTime = 0, lastFrame = 0, time = 0
let planeW, planeH, planeSegX, planeSegY
let uTimePlane, uMVPPlane, uFogColorPlane, uFogNearPlane, uFogFarPlane
let uMVPParticle, uFogColorParticle, uFogNearParticle, uFogFarParticle
// --- Math helpers ---
function mat4() { return new Float32Array(16) }
function mat4Identity(m) {
m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0;
m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0;
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
return m
}
function mat4Multiply(out, a, b) {
for (let i = 0; i < 4; i++)
for (let j = 0; j < 4; j++) {
out[j * 4 + i] = 0
for (let k = 0; k < 4; k++) out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]
}
return out
}
function mat4Perspective(m, fovY, aspect, near, far) {
const f = 1.0 / Math.tan(fovY / 2)
m[0] = f / aspect; m[1] = 0; m[2] = 0; m[3] = 0
m[4] = 0; m[5] = f; m[6] = 0; m[7] = 0
m[8] = 0; m[9] = 0; m[10] = (far + near) / (near - far); m[11] = -1
m[12] = 0; m[13] = 0; m[14] = (2 * far * near) / (near - far); m[15] = 0
return m
}
function mat4RotateX(m, angle) {
const c = Math.cos(angle), s = Math.sin(angle)
const t = mat4Identity(mat4())
t[5] = c; t[6] = s; t[9] = -s; t[10] = c
return mat4Multiply(mat4(), t, m)
}
function mat4Translate(m, x, y, z) {
const t = mat4Identity(mat4())
t[12] = x; t[13] = y; t[14] = z
return mat4Multiply(mat4(), t, m)
}
function randFloat(lo, hi) { return lo + Math.random() * (hi - lo) }
function randFloatSpread(range) { return randFloat(-range / 2, range / 2) }
function degToRad(d) { return d * Math.PI / 180 }
function intToRGB(i) {
return [
((i >> 16) & 0xFF) / 255,
((i >> 8) & 0xFF) / 255,
((i >> 0) & 0xFF) / 255
]
}
// --- Shader sources ---
const PLANE_VERT = `
attribute vec3 aPosition;
uniform mat4 uMVP;
uniform float uTime;
varying float vDist;
void main() {
float dist = sqrt(aPosition.x*aPosition.x + aPosition.z*aPosition.z);
float wave = sin(dist * 0.5 - uTime);
float cave = -exp(-dist * 0.1) * 3.5;
vec3 pos = vec3(aPosition.x, aPosition.y + wave + cave, aPosition.z);
vDist = length((uMVP * vec4(pos, 1.0)).xyz);
gl_Position = uMVP * vec4(pos, 1.0);
}
`
const PLANE_FRAG = `
precision mediump float;
uniform vec3 uFogColor;
uniform float uFogNear;
uniform float uFogFar;
varying float vDist;
void main() {
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
vec3 color = mix(vec3(${intToRGB(COLOR_FOREGROUND).join(',')}), uFogColor, fog);
gl_FragColor = vec4(color, 1.0);
}
`
const PARTICLE_VERT = `
attribute vec3 aPosition;
attribute vec4 aColor;
uniform mat4 uMVP;
varying vec4 vColor;
varying float vDist;
void main() {
vec4 pos = uMVP * vec4(aPosition, 1.0);
vDist = length(pos.xyz);
vColor = aColor;
gl_PointSize = 3.0;
gl_Position = pos;
}
`
const PARTICLE_FRAG = `
precision mediump float;
uniform vec3 uFogColor;
uniform float uFogNear;
uniform float uFogFar;
varying vec4 vColor;
varying float vDist;
void main() {
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
float alpha = vColor.a * (1.0 - fog);
gl_FragColor = vec4(mix(vColor.rgb, uFogColor, fog), alpha);
}
`
function compileShader(src, type) {
const s = gl.createShader(type)
gl.shaderSource(s, src)
gl.compileShader(s)
return s
}
function createProgram(vert, frag) {
const p = gl.createProgram()
gl.attachShader(p, compileShader(vert, gl.VERTEX_SHADER))
gl.attachShader(p, compileShader(frag, gl.FRAGMENT_SHADER))
gl.linkProgram(p)
return p
}
function buildPlane(ratio) {
const scale = 24
planeSegX = Math.round(12 * ratio)
planeSegY = 12
planeW = scale * ratio
planeH = scale
const nx = planeSegX + 1, ny = planeSegY + 1
planeVerts = new Float32Array(nx * ny * 3)
planeOrig = new Float32Array(nx * ny * 3)
let vi = 0
for (let iy = 0; iy < ny; iy++) {
for (let ix = 0; ix < nx; ix++) {
const x = (ix / planeSegX - 0.5) * planeW
const z = (iy / planeSegY - 0.5) * planeH
planeVerts[vi] = x
planeVerts[vi + 1] = 0
planeVerts[vi + 2] = z
planeOrig[vi] = x
planeOrig[vi + 1] = 0
planeOrig[vi + 2] = z
vi += 3
}
}
// Wireframe indices — two triangles per quad
const lines = []
for (let iy = 0; iy < ny; iy++) {
for (let ix = 0; ix < nx; ix++) {
const idx = iy * nx + ix
if (ix < planeSegX) { lines.push(idx, idx + 1) }
if (iy < planeSegY) { lines.push(idx, idx + nx) }
if (ix < planeSegX && iy < planeSegY) { lines.push(idx, idx + nx + 1) }
}
}
planeIdx = new Uint16Array(lines)
}
function spawnParticle(i) {
const x = randFloat(-planeW, planeW)
const z = randFloat(-planeH, planeH)
particleData[i] = {
x, y: 0, z,
vx: randFloatSpread(0.05),
vy: randFloat(0.02, 0.05),
vz: randFloatSpread(0.05),
life: randFloat(3.0, 6.0),
maxLife: 0
}
particleData[i].maxLife = particleData[i].life
const p = i * 3
particlePos[p] = x; particlePos[p + 1] = 0; particlePos[p + 2] = z
const c = i * 4
const [r, g, b] = intToRGB(COLOR_PARTICLE)
particleColor[c + 0] = r
particleColor[c + 1] = g
particleColor[c + 2] = b
particleColor[c + 3] = 0
}
function init() {
canvas.width = Math.floor(window.innerWidth * 0.25)
canvas.height = Math.floor(window.innerHeight * 0.25)
canvas.style.width = window.innerWidth + 'px'
canvas.style.height = window.innerHeight + 'px'
canvas.style.imageRendering = 'pixelated'
const ratio = canvas.width / canvas.height
gl.viewport(0, 0, canvas.width, canvas.height)
gl.clearColor(...intToRGB(COLOR_BACKGROUND), 1)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
buildPlane(ratio)
// Upload plane buffers
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.bufferData(gl.ARRAY_BUFFER, planeVerts, gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, planeOrigBuf)
gl.bufferData(gl.ARRAY_BUFFER, planeOrig, gl.STATIC_DRAW)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, planeIdx, gl.STATIC_DRAW)
// Init particles if first time
if (particleData.length === 0) {
particlePos = new Float32Array(PARTICLE_COUNT * 3)
particleColor = new Float32Array(PARTICLE_COUNT * 4)
for (let i = 0; i < PARTICLE_COUNT; i++) spawnParticle(i)
}
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.bufferData(gl.ARRAY_BUFFER, particlePos, gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.bufferData(gl.ARRAY_BUFFER, particleColor, gl.DYNAMIC_DRAW)
// Build MVP
const proj = mat4Perspective(mat4(), degToRad(70), ratio, 0.1, 100)
let view = mat4Identity(mat4())
view = mat4Translate(view, 0, -7.5, -15)
view = mat4RotateX(view, degToRad(33.75))
const mvp = mat4Multiply(mat4(), proj, view)
gl.useProgram(planeProg)
gl.uniformMatrix4fv(uMVPPlane, false, mvp)
gl.uniform3f(uFogColorPlane, ...intToRGB(COLOR_BACKGROUND))
gl.uniform1f(uFogNearPlane, 2)
gl.uniform1f(uFogFarPlane, 22)
gl.useProgram(particleProg)
gl.uniformMatrix4fv(uMVPParticle, false, mvp)
gl.uniform3f(uFogColorParticle, ...intToRGB(COLOR_BACKGROUND))
gl.uniform1f(uFogNearParticle, 6)
gl.uniform1f(uFogFarParticle, 22)
}
function updatePlane() {
for (let i = 0; i < planeVerts.length; i += 3) {
const x = planeOrig[i], z = planeOrig[i + 2]
const dist = Math.sqrt(x * x + z * z)
planeVerts[i] = x
planeVerts[i + 1] = Math.sin(dist * 0.5 - time) * 0.5 + (-Math.exp(-dist * 0.1) * 3.5)
planeVerts[i + 2] = z
}
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, planeVerts)
}
function updateParticles(delta) {
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = particleData[i]
p.life -= delta
if (p.life <= 0) { spawnParticle(i); continue }
const t = p.life / p.maxLife
p.x += p.vx * delta * 20
p.y += p.vy * delta * 20
p.z += p.vz * delta * 20
const pi = i * 3
particlePos[pi] = p.x; particlePos[pi + 1] = p.y; particlePos[pi + 2] = p.z
particleColor[i * 4 + 3] = Math.min(t * 1.2, 1)
}
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleColor)
}
function drawPlane() {
gl.useProgram(planeProg)
gl.uniform1f(uTimePlane, time)
const aPos = gl.getAttribLocation(planeProg, 'aPosition')
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.enableVertexAttribArray(aPos)
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
gl.drawElements(gl.LINES, planeIdx.length, gl.UNSIGNED_SHORT, 0)
}
function drawParticles() {
gl.useProgram(particleProg)
const aPos = gl.getAttribLocation(particleProg, 'aPosition')
const aCol = gl.getAttribLocation(particleProg, 'aColor')
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.enableVertexAttribArray(aPos)
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.enableVertexAttribArray(aCol)
gl.vertexAttribPointer(aCol, 4, gl.FLOAT, false, 0, 0)
gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT)
}
function animate(now) {
requestAnimationFrame(animate)
if (now - lastFrame < FRAME_TIME) return
const delta = (now - lastTime) * 0.00008 || 0
lastFrame = now; lastTime = now; time += delta
gl.clear(gl.COLOR_BUFFER_BIT)
updatePlane()
updateParticles(delta)
drawPlane()
drawParticles()
}
// --- Bootstrap ---
canvas = document.createElement('canvas')
gl = canvas.getContext('webgl')
planeProg = createProgram(PLANE_VERT, PLANE_FRAG)
particleProg = createProgram(PARTICLE_VERT, PARTICLE_FRAG)
planeBuf = gl.createBuffer()
planeOrigBuf = gl.createBuffer()
planeIdxBuf = gl.createBuffer()
particlePosBuf = gl.createBuffer()
particleColorBuf = gl.createBuffer()
uTimePlane = gl.getUniformLocation(planeProg, 'uTime')
uMVPPlane = gl.getUniformLocation(planeProg, 'uMVP')
uFogColorPlane = gl.getUniformLocation(planeProg, 'uFogColor')
uFogNearPlane = gl.getUniformLocation(planeProg, 'uFogNear')
uFogFarPlane = gl.getUniformLocation(planeProg, 'uFogFar')
uMVPParticle = gl.getUniformLocation(particleProg, 'uMVP')
uFogColorParticle = gl.getUniformLocation(particleProg, 'uFogColor')
uFogNearParticle = gl.getUniformLocation(particleProg, 'uFogNear')
uFogFarParticle = gl.getUniformLocation(particleProg, 'uFogFar')
if (window.outerWidth >= 1024) {
window.addEventListener('resize', init)
init()
animate(0)
} else {
init()
}
return canvas
})())
@@ -0,0 +1,344 @@
(() => {
const encodeSVG /**/ = svg => "data:image/svg+xml;base64," + btoa(svg)
const formatDate /**/ = str => new Date(str).toDateString().slice(4).toUpperCase()
const icons = {
// NOTE: External Images should have the version parameter appended to them.
// e.g. /public/.../image.png?v=${dataVersion}
diamond_fill: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="8,0 16,8 8,16 0,8" style="fill:#a0a0a0"/></svg>`),
diamond_empty: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,0L0,8l8,8,8-8L8,0ZM3,8l5-5,5,5-5,5-5-5Z" style="fill: #a0a0a0"/></svg>`),
icon_cross: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon style="fill:#c0c0c0" points="0 0 0 3 6 8 0 13 0 16 3 16 8 10 13 16 16 16 16 13 10 8 16 3 16 0 13 0 8 6 3 0 0 0"/></svg>`),
icon_rss: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><polygon style="fill:#c0c0c0" points="6.4 64 0 57.6 6.4 48 16 57.6 6.4 64"/><polyline style="fill:#c0c0c0" points="25.6 64 38.4 64 33.6 30.4 0 25.6 0 38.4 20.8 43.2"/><polyline style="fill:#c0c0c0" points="51.2 64 64 64 59.2 4.8 0 0 0 12.8 46.4 17.6"/></svg>`),
icon_top: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><polygon style="fill:#c0c0c0" points="1 0 31 0 32 6 0 6 1 0"/><polygon style="fill:#c0c0c0" points="0 32 0 24 16 8 32 24 32 32 16 20 0 32"/></svg>`),
tags_development: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon style="fill:#c0c0c0" points="8 2 5 2 4 0 3 2 0 2 2 4 0 8 4 6 8 8 6 4 8 2"/></svg>`),
tags_personal: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 13"><polygon style="fill:#c0c0c0" points="9 13 0 4 4 0 9 3 14 0 18 4 9 13"/></svg>`),
author_bakonpancakz: /**/ `data:image/gif;base64,R0lGODlhRABOAPAAAAAAAMDAwCH5BAUKAAAALAAAAABEAE4AAAL/hI+pF+0P45p0uQqi3hD79CjcSF6fZzLlyp1iCrKy5KphPOe3SyP6D0N1fMBioGYxKpFE5ZKZcTqR0urxZM0Ks9Utd1r5WimksFgzKWNy5FHavZ61W0m6eeXt4TYfVp9fBxhX8of2AjeoVrjTZHenmNcQKPiIGHm1RzkHmRhkQHhp2Mlo47hpeUr66TeJV6kadaaZCSurdSjVmIsLtjrG++TbBQzUamT887arWxQqR1s8qkOcLM0GXR0rWRqNbZvN/N3NLd7sXf4cjn59vg7a7o4qHJ+uTs85f+8Krz/E3+9JG0B8AgeaymdQlL2EnhgSdCivIMSAE2dJrIgMI7mKPow4OvJICaRCkXpI+rvokdpEZezSTYvyj5SMjWq20ZS3D6Ggjjkz2sy05RwVPTxgQTk6NCDSpYt+Mn2aB0EBADs=`,
}
// Parse Data from Webpage //
const dataVersion /**/ = document.querySelector("data[key='version']").value
const dataManifest /**/ = JSON.parse(document.querySelector("data[key='manifest']").value)
{
// Additional Validation //
if (!dataManifest.articles) /**/ throw "Missing Key: .articles" // Type: Metadata
if (!dataManifest.authors) /**/ throw "Missing Key: .authors" // Type: Filter
if (!dataManifest.links) /**/ throw "Missing Key: .links" // Type: Metadata
if (!dataManifest.tags) /**/ throw "Missing Key: .tags" // Type: Filter
const lookupAuthor /**/ = new Set(dataManifest.authors.map(t => t.id))
const lookupTags /**/ = new Set(dataManifest.tags.map(t => t.id))
console.info("Authors IDs:", [...lookupAuthor].join(", "))
console.info("Tags IDs: ", [...lookupTags].join(", "))
dataManifest.articles = dataManifest.articles.filter((a, i) => {
if (!a.date) {
console.warn(`Article '${a.id}' has no date and will be hidden.`)
return false
}
for (const t of a.tags) {
if (!lookupTags.has(t)) {
console.warn(`Article '${a.id}' has an unknown tag id (${t})`)
return false
}
}
if (!lookupAuthor.has(a.author)) {
console.warn(`Article '${a.id}' has an unknown author id (${a.author})`)
return false
}
console.info(`Article OK: ${a.id}`)
return true
})
}
// Render Functions //
let articleHeader /**/ = document.querySelector("div.article-header")
let articleContent /**/ = document.querySelector("div.article-content")
let navigation /**/ = document.querySelector("nav.section-navigation")
let listArticles /**/ = document.querySelector("div.article-list")
let listChapters /**/ = null
function updateNavigation() {
function templateLink(link, name, description, icon) {
const image = icon.startsWith("script:")
? icons[icon.slice(7)]
: icon // use uri
const html = `
<a class="item-nav animation-fadein" href="${link}">
<div class="item-nav-text">
<p class="animation-scrollin item-nav-text-anchor">${name}</p>
<p class="animation-scrollin item-nav-text-content">${description}</p>
</div>
<div class="item-nav-icon">
<div class="item-nav-icon-background"></div>
<img class="item-nav-icon-foreground" alt="Icon for ${name}" src="${image}">
</div>
</a>
`
return html
}
function templateList(name) {
let htmlKey /**/ = name.toLocaleLowerCase()
let dataKey /**/ = `blog_togglestate_${htmlKey}`
let result /**/ = localStorage.getItem(dataKey)
let active /**/ = (result === null) ? true : (result === "true")
// Create Toggle Buttons //
navigation.insertAdjacentHTML("beforeend", `
<button class="nav-header" for="${htmlKey}">
<span>${name}</span>
<img alt="Toggle open state for list ${name}">
</button>
<div class="nav-list" id="${htmlKey}">
</div>
`)
const list /**/ = navigation.querySelector(`div.nav-list#${htmlKey}`)
const header /**/ = navigation.querySelector(`button.nav-header[for="${htmlKey}"]`)
const icon /**/ = header.querySelector("img")
function update() {
localStorage.setItem(dataKey, active)
list.style.display = active
? "" // reset
: "none"
icon.src = active
? icons.diamond_fill
: icons.diamond_empty
active
? header.classList.add("open")
: header.classList.remove("open")
}
header.onclick = () => {
active = !active
update()
}
update()
return list
}
// Render Nav Links //
templateList("LINKS").innerHTML = dataManifest.links
.map(l => templateLink(l.id, l.name, l.description, l.icon))
.join("\n")
templateList("TAGS").innerHTML = dataManifest.tags
.map(t => templateLink(`/blog/?filter[tag]=${t.id}`, t.name, t.description, t.icon))
.join("\n")
templateList("AUTHORS").innerHTML = dataManifest.authors
.map(a => templateLink(`/blog/?filter[author]=${a.id}`, a.name, a.description, a.icon))
.join("\n")
listChapters = templateList("CHAPTERS")
}
updateNavigation()
function updateArticle() {
articleContent.replaceChildren()
articleHeader.replaceChildren()
listArticles.replaceChildren()
listChapters.replaceChildren()
const slug /**/ = new URL(window.location.href).pathname.slice("/blog/".length)
const article /**/ = dataManifest.articles.find(a => a.id == slug)
if (article) {
// Display Article //
document.title = `panca.kz - Blog - ${article.title}`
// Download Article Content [ASYNC] //
fetch(`/public/blog/${slug}/content.html?v=${dataVersion}`)
.then(async resp => {
// Insert Article Content //
// Yes, we are exploiting ourselves (safely). But I want the ability to
// implement custom elements and features to specific articles in the future.
if (!resp.ok) {
throw `Server responded with ${resp.status} ${resp.statusText}`
}
articleContent.insertAdjacentHTML("beforeend", await resp.text())
articleContent.querySelectorAll("script").forEach(s => {
eval(s.innerHTML)
})
// Generate Article Chapters //
let chapterHTML = ""
for (const item of articleContent.children) {
const normalize = c => {
const v = c.textContent.toLocaleLowerCase().replaceAll(" ", "-")
c.id = v
return v
}
if (item.classList.contains("element-header")) {
chapterHTML += `<a class="animation-scrollin chapter" href="#${normalize(item)}">${item.textContent}</a>\n`
}
if (item.classList.contains("element-subheader")) {
chapterHTML += `<a class="animation-scrollin section" href="#${normalize(item)}">${item.textContent}</a>\n`
}
}
listChapters.innerHTML = chapterHTML
})
.catch(err => {
// Generic Fetch Error //
console.error("<Cannot Download Article!>", err)
articleContent.innerHTML += `
<p style="color: red; text-align: center;" class="animation-blink">
Fetch Error, please see console for more information.
</p>
`
})
// Render Article Header (Metadata) //
const author /**/ = dataManifest.authors.find(a => a.id === article.author)
const tags /**/ = article.tags.map(id => dataManifest.tags.find(t => t.id === id))
listChapters.innerHTML += `
<p class="nav-message">... LOADING ARTICLE ...</p>
`
articleHeader.innerHTML += `
<img
id="header-banner"
fetchpriority="high"
alt="Banner for ${article.title}"
src="${article.banner_main}?v=${dataVersion}">
<h1 class="effect-docked" id="header-title">
${article.title}
</h1>
<div class="header-chevrons">
<div class="effect-chevron-container" id="header-about">
<span class="effect-chevron-point-right" id="article-author">
${author.name}
</span>
<span class="effect-chevron-point-right" id="article-published">
${formatDate(article.date)}
</span>
</div>
<div class="effect-chevron-container" id="header-tags">
${tags.map(t => `<span class="effect-chevron-point-left">${t.name}</span>`).join("\n")}
</div>
</div>
`
} else {
// Display Search //
document.title = `panca.kz - Blog`
// Collect Filters and Articles //
const params = new URLSearchParams(window.location.search)
const filterTags = new Set(params
.getAll("filter[tag]")
.filter(s => dataManifest.tags.findIndex(t => t.id.localeCompare(s) === 0) > -1)
)
const filterAuthor = new Set(params
.getAll("filter[author]")
.filter(s => dataManifest.authors.findIndex(t => t.id.localeCompare(s) === 0) > -1)
)
const results = dataManifest.articles
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.filter(a => {
// Need Requested Author //
if (filterAuthor.size > 0) {
if (!filterAuthor.has(a.author)) return false
}
// Need Requested Tags //
if (filterTags.size > 0) {
let missedTag = false
filterTags.forEach(t => {
if (a.tags.findIndex(s => s === t) === -1) {
missedTag = true
}
})
if (missedTag) {
return false
}
}
return true
})
// Render Results //
listChapters.innerHTML += `
<p class="nav-message">... SELECT ARTICLE ...</p>
`
listArticles.innerHTML += `
<div class="item-article-count">
<p class="animation-scrollin item-article-count-text">
${results.length} RESULT${results.length === 1 ? "" : "s"}
${new Array(...filterTags.values()).map(s => `&lt;TAG:${s.toUpperCase()}&gt;`).join(" ")}
${new Array(...filterAuthor.values()).map(s => `&lt;AUTHOR:${s.toUpperCase()}&gt;`).join(" ")}
<span class="animation-blink item-article-count-cursor">
_
</span>
</p>
</div>
`
listArticles.innerHTML += results.map((a, i) => `
<a class="item-article" href="/blog/${a.id}" style="animation-delay: ${i * 200}ms;">
<div class="item-article-layer">
<img
class="item-article-banner"
onload="this.classList.add('animation-fadein')"
alt="Banner for ${a.title}"
src="${a.banner_list}?v=${dataVersion}">
</div>
<div class="item-article-layer item-article-meta">
<p class="animation-scrollin item-article-meta-title">${a.title}</p>
<p class="animation-fadein item-article-meta-description">${a.description}</p>
<div class="animation-fadein item-article-meta-tags">
<span>${formatDate(a.date)}</span>
${a.tags.map(t => `<span>${t.toUpperCase()}</span>`).join("\n")}
</div>
</div>
</a>
`)
.join("\n")
}
}
updateArticle()
// History Overrides //
window.addEventListener("popstate", () => {
updateArticle()
})
document.addEventListener("click", (e) => {
const link = e.target.closest("a")
if (!link) return
const url = new URL(link.href)
// Ignore non-blog links
if (!url.pathname.startsWith("/blog/")) return
if (url.pathname.startsWith("/blog/rss.xml")) return
// Handle chapter links
const current = new URL(window.location.href)
if (
url.pathname === current.pathname &&
url.search === current.search &&
url.hash
) {
// Smooth scroll into section
e.preventDefault()
const target = document.querySelector(url.hash)
if (target) {
target.scrollIntoView({ behavior: "smooth" })
}
return
}
// Update Application
e.preventDefault()
history.pushState(null, "", url.pathname + url.search + url.hash)
updateArticle()
})
})()
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,80 @@
<h2 class="element-header">Introduction</h2>
<p class="element-paragraph">
If you're reading this... I haven't written the article yet :P
</p>
<p class="element-paragraph">
Luctus montes donec mauris. Vehicula suspendisse condimentum massa fringilla at conubia.
Senectus laoreet praesent in, porttitor sollicitudin luctus orci scelerisque egestas malesuada faucibus a.
Arcu imperdiet libero imperdiet nulla libero natoque eu fusce ultricies vehicula nisi.
Torquent lacus sodales inceptos dignissim sed, mauris feugiat erat.
Phasellus at tincidunt sociosqu potenti risus aliquam non litora.
Commodo mi fermentum, id arcu dictum convallis.
Vehicula convallis, proin fames molestie potenti congue penatibus feugiat porttitor.
Habitant justo lacinia, curabitur nascetur!
Dolor nunc accumsan imperdiet penatibus?
Amet cursus faucibus dui vestibulum nascetur quam.
</p>
<div class="element-divider"></div>
<h2 class="element-header">Prerequisites</h2>
<p class="element-paragraph">
Praesent augue ligula amet nisl. Litora lectus consectetur aliquet.
Sociis iaculis dolor nostra pulvinar felis condimentum netus.
Suscipit enim habitant sed eros etiam, ut odio sed.
Fringilla quam tempus nisl natoque parturient dolor curabitur tellus.
Lacus conubia quis inceptos tristique ultricies cum consequat.
Consequat phasellus proin dolor molestie nec quam cursus mollis vel?
Eros tempor sollicitudin tortor litora nunc.
Sem scelerisque semper id ac auctor fusce quis erat imperdiet convallis donec eget!
Pellentesque tempor nunc ipsum imperdiet quam ipsum habitant feugiat.
</p>
<h3 class="element-subheader">Hosting</h3>
<p class="element-paragraph">
Odio nibh penatibus erat sociosqu eget iaculis. Ante curabitur bibendum ligula?
Netus aliquet cursus interdum pharetra. Magnis elementum nunc luctus eu nam sit habitant etiam vestibulum in.
Lacus nisl ullamcorper molestie! Dignissim, vulputate porta pulvinar duis posuere.
</p>
<h3 class="element-subheader">Domains</h3>
<p class="element-paragraph">
Curae; laoreet cum duis turpis, feugiat per integer.
Sit in blandit himenaeos. Penatibus fringilla imperdiet, luctus dapibus elementum.
Felis senectus pulvinar a natoque risus.
</p>
<h3 class="element-subheader">Credentials</h3>
<p class="element-paragraph">
Mauris habitasse nulla accumsan fermentum fermentum.
Magnis amet amet risus senectus in curae;.
Elementum ac montes sagittis vivamus malesuada elementum senectus ultrices scelerisque fusce.
In nulla platea.
</p>
<div class="element-divider"></div>
<h2 class="element-header">Setup DNS</h2>
<p class="element-paragraph">
Nisi vitae vehicula etiam nam auctor, accumsan purus ridiculus molestie.
Ante malesuada condimentum aliquam etiam auctor arcu porttitor parturient nullam accumsan bibendum?
Vitae arcu at mattis. Volutpat himenaeos class vitae elit, luctus sociosqu semper?
Praesent taciti lobortis tortor laoreet magnis tortor sagittis porta facilisi vivamus dictum scelerisque.
Consectetur elit enim malesuada commodo proin facilisi sagittis!
Malesuada quam duis non elit himenaeos sem.
Luctus suspendisse senectus mollis aliquet aliquam pellentesque conubia condimentum suspendisse aptent litora potenti.
Curabitur sociis lacus turpis, laoreet ligula consequat luctus eu auctor auctor sagittis.
Turpis nibh feugiat lorem donec quis bibendum.
Himenaeos sapien interdum aptent.
</p>
<h2 class="element-header">Setup Linux</h2>
<p class="element-paragraph">
Convallis pharetra vitae tincidunt phasellus bibendum porta, praesent dictum aliquet volutpat.
Lobortis semper eget consectetur elementum elit hendrerit natoque mauris taciti ante ac augue.
Ullamcorper viverra vestibulum vehicula nisi metus vestibulum curabitur vehicula et.
Phasellus ligula vitae posuere penatibus ullamcorper lorem phasellus metus.
Scelerisque ac lobortis gravida primis?
Lacus porttitor ullamcorper quisque viverra molestie.
Lacinia bibendum facilisi tempor cubilia felis eros!
Etiam blandit lacinia leo urna.
</p>