Initial Release
This commit is contained in:
@@ -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 |
@@ -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 => `<TAG:${s.toUpperCase()}>`).join(" ")}
|
||||
${new Array(...filterAuthor.values()).map(s => `<AUTHOR:${s.toUpperCase()}>`).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>
|
||||
Reference in New Issue
Block a user