Files
2026-06-11 01:30:39 +07:00

965 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GIS Portfolio — Sistem Informasi Geografis</title>
<link
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@300;400;500;600&family=DM+Mono:wght@300;400;500&display=swap"
rel="stylesheet">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--forest: #1C3A2E;
--parchment: #F7F0E3;
--cream: #FDF8F0;
--terra: #C9734A;
--sage: #8AAF9A;
--ink: #1A1A1A;
--mist: #D6E4DC;
--gold: #C9A96E;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--cream);
color: var(--ink);
font-family: 'Inter', sans-serif;
overflow-x: hidden;
}
/* ─── NAV ─── */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.2rem 3rem;
background: rgba(253, 248, 240, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(28, 58, 46, 0.08);
}
.nav-logo {
font-family: 'DM Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.15em;
color: var(--forest);
text-transform: uppercase;
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
font-size: 0.8rem;
letter-spacing: 0.08em;
color: var(--forest);
text-decoration: none;
opacity: 0.7;
transition: opacity 0.2s;
text-transform: uppercase;
}
.nav-links a:hover {
opacity: 1;
}
/* ─── HERO ─── */
.hero {
min-height: 100vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: var(--forest);
}
.topo-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.hero-content {
position: relative;
z-index: 2;
text-align: center;
padding: 2rem;
max-width: 780px;
}
.hero-eyebrow {
font-family: 'DM Mono', monospace;
font-size: 0.7rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--sage);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.hero-eyebrow::before,
.hero-eyebrow::after {
content: '';
display: block;
width: 40px;
height: 1px;
background: var(--sage);
opacity: 0.5;
}
.hero-title {
font-family: 'Playfair Display', serif;
font-size: clamp(2.8rem, 7vw, 5.5rem);
font-weight: 700;
color: var(--parchment);
line-height: 1.1;
margin-bottom: 0.3rem;
}
.hero-title em {
font-style: italic;
color: var(--gold);
}
.hero-subtitle {
font-family: 'Playfair Display', serif;
font-style: italic;
font-size: clamp(1rem, 2.5vw, 1.4rem);
color: var(--sage);
margin-bottom: 2rem;
font-weight: 400;
}
.hero-desc {
font-size: 0.9rem;
line-height: 1.8;
color: rgba(247, 240, 227, 0.65);
max-width: 520px;
margin: 0 auto 3rem;
}
.hero-cta {
display: inline-flex;
align-items: center;
gap: 0.6rem;
padding: 0.85rem 2rem;
background: transparent;
border: 1px solid var(--gold);
color: var(--gold);
font-family: 'DM Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
text-decoration: none;
border-radius: 2px;
transition: background 0.3s, color 0.3s;
}
.hero-cta:hover {
background: var(--gold);
color: var(--forest);
}
.hero-cta svg {
width: 14px;
height: 14px;
}
.scroll-hint {
position: absolute;
bottom: 2.5rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--sage);
opacity: 0.6;
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.2em;
text-transform: uppercase;
animation: bounce 2.5s ease-in-out infinite;
}
.scroll-hint svg {
width: 16px;
height: 16px;
}
@keyframes bounce {
0%,
100% {
transform: translateX(-50%) translateY(0);
}
50% {
transform: translateX(-50%) translateY(6px);
}
}
/* ─── INTRO STRIP ─── */
.intro-strip {
background: var(--parchment);
padding: 5rem 3rem;
text-align: center;
border-bottom: 1px solid var(--mist);
}
.intro-strip h2 {
font-family: 'Playfair Display', serif;
font-size: clamp(1.5rem, 3vw, 2.2rem);
color: var(--forest);
margin-bottom: 1rem;
font-weight: 600;
}
.intro-strip p {
font-size: 0.95rem;
line-height: 1.9;
color: rgba(26, 26, 26, 0.6);
max-width: 600px;
margin: 0 auto;
}
/* ─── PROJECTS ─── */
.projects {
padding: 6rem 0;
background: var(--cream);
}
.section-label {
font-family: 'DM Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--terra);
text-align: center;
margin-bottom: 4rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
}
.section-label::before,
.section-label::after {
content: '';
flex: 1;
max-width: 80px;
height: 1px;
background: var(--terra);
opacity: 0.3;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
max-width: 1180px;
margin: 0 auto;
padding: 0 2rem;
}
/* ─── PROJECT CARD ─── */
.card {
background: #fff;
border-radius: 4px;
overflow: hidden;
border: 1px solid rgba(28, 58, 46, 0.08);
transition: transform 0.35s ease, box-shadow 0.35s ease;
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 24px 48px rgba(28, 58, 46, 0.1);
}
.card-visual {
height: 220px;
position: relative;
overflow: hidden;
}
/* Card 1 — multi-layer city */
.card-visual-1 {
background: #0f2318;
}
/* Card 2 — choropleth */
.card-visual-2 {
background: #1a2035;
}
/* Card 3 — social aid */
.card-visual-3 {
background: #1e1825;
}
.card-visual canvas,
.card-visual svg {
width: 100%;
height: 100%;
display: block;
}
.card-num {
position: absolute;
top: 1rem;
left: 1.2rem;
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.2em;
color: rgba(255, 255, 255, 0.35);
}
.card-tag {
position: absolute;
top: 1rem;
right: 1.2rem;
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.1em;
padding: 0.25rem 0.6rem;
border-radius: 2px;
color: #fff;
}
.tag-green {
background: rgba(138, 175, 154, 0.25);
border: 1px solid rgba(138, 175, 154, 0.4);
}
.tag-blue {
background: rgba(100, 140, 200, 0.25);
border: 1px solid rgba(100, 140, 200, 0.4);
}
.tag-purple {
background: rgba(160, 110, 200, 0.25);
border: 1px solid rgba(160, 110, 200, 0.4);
}
.card-body {
padding: 1.8rem 1.8rem 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.card-title {
font-family: 'Playfair Display', serif;
font-size: 1.2rem;
font-weight: 600;
color: var(--forest);
margin-bottom: 0.6rem;
line-height: 1.3;
}
.card-desc {
font-size: 0.83rem;
line-height: 1.75;
color: rgba(26, 26, 26, 0.58);
margin-bottom: 1.4rem;
flex: 1;
}
.card-layers {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 1.6rem;
}
.layer-chip {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-family: 'DM Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.05em;
padding: 0.2rem 0.55rem;
border-radius: 20px;
background: var(--mist);
color: var(--forest);
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.card-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-family: 'DM Mono', monospace;
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--terra);
padding-bottom: 2px;
border-bottom: 1px solid transparent;
transition: border-color 0.2s, gap 0.2s;
align-self: flex-start;
}
.card-link:hover {
border-color: var(--terra);
gap: 0.8rem;
}
.card-link svg {
width: 12px;
height: 12px;
}
/* ─── FOOTER ─── */
footer {
background: var(--forest);
padding: 3rem;
text-align: center;
}
.footer-inner {
max-width: 600px;
margin: 0 auto;
}
.footer-title {
font-family: 'Playfair Display', serif;
font-style: italic;
font-size: 1.3rem;
color: var(--parchment);
margin-bottom: 0.5rem;
}
.footer-sub {
font-family: 'DM Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.2em;
color: var(--sage);
opacity: 0.6;
margin-bottom: 1.5rem;
text-transform: uppercase;
}
.footer-divider {
width: 40px;
height: 1px;
background: var(--gold);
opacity: 0.3;
margin: 1.5rem auto;
}
.footer-note {
font-size: 0.75rem;
color: rgba(247, 240, 227, 0.3);
letter-spacing: 0.05em;
}
/* ─── ANIMATIONS ─── */
.fade-up {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.fade-up.visible {
opacity: 1;
transform: translateY(0);
}
.fade-up:nth-child(2) {
transition-delay: 0.1s;
}
.fade-up:nth-child(3) {
transition-delay: 0.2s;
}
</style>
</head>
<body>
<!-- NAV -->
<nav>
<span class="nav-logo">GIS · Kota Pontianak</span>
<ul class="nav-links">
<li><a href="https://git.ifuntanhub.dev/denavy/D1041231027-WebGIS" target="_blank">Source</a></li>
</ul>
</nav>
<!-- HERO -->
<section class="hero">
<canvas class="topo-canvas" id="topoCanvas"></canvas>
<div class="hero-content">
<p class="hero-eyebrow">Sistem Informasi Geografis</p>
<h1 class="hero-title">Membaca Bumi<br>Lewat <em>Data</em></h1>
<p class="hero-subtitle">Kota Pontianak dalam lapisan-lapisan peta</p>
<p class="hero-desc">
Kumpulan proyek WebGIS interaktif yang memetakan infrastruktur kota,
demografi penduduk, hingga analisis distribusi bantuan sosial berbasis
kecerdasan spasial.
</p>
<a class="hero-cta" href="#projects">
Jelajahi Proyek
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 8h10M9 4l4 4-4 4" />
</svg>
</a>
</div>
</section>
<!-- INTRO -->
<div class="intro-strip">
<h2>Tiga Proyek, Satu Kota</h2>
<p>
Setiap proyek mendekati Pontianak dari sudut yang berbeda, infrastruktur,
kependudukan, dan kesejahteraan sosial. Namun semuanya berbicara satu bahasa,
geografi sebagai alat analisis dan pemahaman.
</p>
</div>
<!-- PROJECTS -->
<section class="projects" id="projects">
<p class="section-label">Proyek Utama</p>
<div class="projects-grid">
<!-- Card 1 -->
<article class="card fade-up">
<div class="card-visual card-visual-1">
<canvas id="c1" style="width:100%;height:100%;"></canvas>
<span class="card-num">01</span>
<span class="card-tag tag-green">Multi-Layer WebGIS</span>
</div>
<div class="card-body">
<h3 class="card-title">Infrastruktur Kota Pontianak</h3>
<p class="card-desc">
Pemetaan infrastruktur fisik dan tata ruang dengan arsitektur tiga layer
yang dapat diaktifkan secara independen — SPBU, jaringan jalan, dan parsil tanah.
</p>
<div class="card-layers">
<span class="layer-chip"><span class="dot" style="background:#F5C542"></span>SPBU · Point</span>
<span class="layer-chip"><span class="dot" style="background:#6EC4A7"></span>Jalan ·
LineString</span>
<span class="layer-chip"><span class="dot" style="background:#A87CF5"></span>Parsil ·
Polygon</span>
</div>
<a class="card-link" href="http://m4wws0gg8cw80g8ow8gg08ck.203.24.51.230.sslip.io" target="_blank">
Buka Peta
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 8h10M9 4l4 4-4 4" />
</svg>
</a>
</div>
</article>
<!-- Card 2 -->
<article class="card fade-up">
<div class="card-visual card-visual-2">
<canvas id="c2" style="width:100%;height:100%;"></canvas>
<span class="card-num">02</span>
<span class="card-tag tag-blue">Choropleth · Demografi</span>
</div>
<div class="card-body">
<h3 class="card-title">Kepadatan Penduduk Pontianak 2025</h3>
<p class="card-desc">
Visualisasi interaktif tingkat kepadatan penduduk di 6 kecamatan Kota
Pontianak menggunakan gradasi warna choropleth berdasarkan proyeksi 2025.
</p>
<div class="card-layers">
<span class="layer-chip"><span class="dot" style="background:#93C5FD"></span>6 Kecamatan</span>
<span class="layer-chip"><span class="dot" style="background:#3B82F6"></span>Polygon ·
Choropleth</span>
<span class="layer-chip"><span class="dot" style="background:#1D4ED8"></span>Popup
Statistik</span>
</div>
<a class="card-link" href="http://iw8swswsc8go40s8sck0okk8.203.24.51.230.sslip.io" target="_blank">
Buka Peta
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 8h10M9 4l4 4-4 4" />
</svg>
</a>
</div>
</article>
<!-- Card 3 -->
<article class="card fade-up">
<div class="card-visual card-visual-3">
<canvas id="c3" style="width:100%;height:100%;"></canvas>
<span class="card-num">03</span>
<span class="card-tag tag-purple">Analisis Spasial · Turf.js</span>
</div>
<div class="card-body">
<h3 class="card-title">Distribusi Bantuan Sosial</h3>
<p class="card-desc">
Sistem analitik spasial berbasis Turf.js untuk manajemen alokasi bantuan
sosial — rekomendasi otomatis titik distribusi terdekat bagi setiap warga.
</p>
<div class="card-layers">
<span class="layer-chip"><span class="dot" style="background:#E879F9"></span>Titik
Distribusi</span>
<span class="layer-chip"><span class="dot" style="background:#A855F7"></span>Radius
Layanan</span>
<span class="layer-chip"><span class="dot" style="background:#7C3AED"></span>Proximity
Analysis</span>
</div>
<a class="card-link" href="http://s04s08kgsocwocw00ook4ws0.203.24.51.230.sslip.io" target="_blank">
Buka Peta
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 8h10M9 4l4 4-4 4" />
</svg>
</a>
</div>
</article>
</div>
</section>
<!-- FOOTER -->
<footer id="footer">
<div class="footer-inner">
<p class="footer-title">UAS Sistem Informasi Geografis</p>
<p class="footer-sub">Kumpulan Proyek Mata Kuliah SIG</p>
<div class="footer-divider"></div>
<p class="footer-note">Proyek Akademik · WebGIS · Leaflet.js · Turf.js</p>
</div>
</footer>
<script>
/* ─── TOPO HERO ANIMATION ─── */
const heroCanvas = document.getElementById('topoCanvas');
const hctx = heroCanvas.getContext('2d');
let W, H, lines = [], t = 0;
function resize() {
W = heroCanvas.width = heroCanvas.offsetWidth;
H = heroCanvas.height = heroCanvas.offsetHeight;
generateLines();
}
function generateLines() {
lines = [];
const count = 12;
for (let i = 0; i < count; i++) {
const pts = [];
const y0 = (H / count) * i + H * 0.05;
const segs = 14;
for (let s = 0; s <= segs; s++) {
pts.push({
x: (W / segs) * s,
y: y0 + (Math.random() - 0.5) * H * 0.18,
dy: (Math.random() - 0.5) * 0.18,
speed: 0.003 + Math.random() * 0.004,
phase: Math.random() * Math.PI * 2
});
}
lines.push({ pts, alpha: 0.06 + Math.random() * 0.09 });
}
}
function drawTopo(ts) {
t = ts * 0.001;
hctx.clearRect(0, 0, W, H);
lines.forEach(line => {
hctx.beginPath();
hctx.strokeStyle = `rgba(138,175,154,${line.alpha})`;
hctx.lineWidth = 1;
const pts = line.pts.map(p => ({
x: p.x,
y: p.y + Math.sin(t * p.speed * 60 + p.phase) * H * 0.04
}));
hctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length - 1; i++) {
const mx = (pts[i].x + pts[i + 1].x) / 2;
const my = (pts[i].y + pts[i + 1].y) / 2;
hctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
}
hctx.stroke();
});
requestAnimationFrame(drawTopo);
}
window.addEventListener('resize', resize);
resize();
requestAnimationFrame(drawTopo);
/* ─── CARD MINI-VISUALIZATIONS ─── */
// Card 1: Multi-layer city grid with dots, lines, polygons
(function () {
const cv = document.getElementById('c1');
const rect = cv.parentElement.getBoundingClientRect();
cv.width = rect.width || 360;
cv.height = 220;
const ctx = cv.getContext('2d');
let t = 0;
const roads = [
[{ x: 0.05, y: 0.3 }, { x: 0.95, y: 0.35 }],
[{ x: 0.1, y: 0.6 }, { x: 0.9, y: 0.55 }],
[{ x: 0.25, y: 0.05 }, { x: 0.3, y: 0.95 }],
[{ x: 0.65, y: 0.05 }, { x: 0.6, y: 0.95 }],
[{ x: 0.05, y: 0.8 }, { x: 0.45, y: 0.72 }, { x: 0.9, y: 0.82 }],
];
const roadColors = ['#6EC4A7', '#A3D9C5', '#6EC4A7', '#A3D9C5', '#5ABBA0'];
const parcels = [
[{ x: 0.31, y: 0.06 }, { x: 0.59, y: 0.06 }, { x: 0.59, y: 0.29 }, { x: 0.31, y: 0.29 }],
[{ x: 0.31, y: 0.36 }, { x: 0.58, y: 0.36 }, { x: 0.58, y: 0.54 }, { x: 0.31, y: 0.54 }],
[{ x: 0.07, y: 0.36 }, { x: 0.24, y: 0.36 }, { x: 0.24, y: 0.58 }, { x: 0.07, y: 0.58 }],
[{ x: 0.62, y: 0.36 }, { x: 0.88, y: 0.36 }, { x: 0.88, y: 0.54 }, { x: 0.62, y: 0.54 }],
];
const parcelColors = ['rgba(168,124,245,0.18)', 'rgba(168,124,245,0.12)', 'rgba(168,124,245,0.22)', 'rgba(168,124,245,0.15)'];
const spbu = [{ x: 0.18, y: 0.2 }, { x: 0.72, y: 0.45 }, { x: 0.45, y: 0.7 }, { x: 0.82, y: 0.72 }, { x: 0.1, y: 0.82 }];
const spbu24 = [true, false, true, false, true];
function draw(ts) {
t = ts * 0.001;
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// grid
ctx.strokeStyle = 'rgba(100,200,150,0.05)';
ctx.lineWidth = 1;
for (let i = 0; i < W; i += 30) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, H); ctx.stroke(); }
for (let j = 0; j < H; j += 30) { ctx.beginPath(); ctx.moveTo(0, j); ctx.lineTo(W, j); ctx.stroke(); }
// parcels
parcels.forEach((pts, i) => {
ctx.beginPath();
ctx.moveTo(pts[0].x * W, pts[0].y * H);
pts.slice(1).forEach(p => ctx.lineTo(p.x * W, p.y * H));
ctx.closePath();
ctx.fillStyle = parcelColors[i];
ctx.fill();
ctx.strokeStyle = 'rgba(168,124,245,0.35)';
ctx.lineWidth = 1;
ctx.stroke();
});
// roads
roads.forEach((pts, i) => {
ctx.beginPath();
ctx.moveTo(pts[0].x * W, pts[0].y * H);
pts.slice(1).forEach(p => ctx.lineTo(p.x * W, p.y * H));
ctx.strokeStyle = roadColors[i];
ctx.lineWidth = 1.5;
ctx.stroke();
});
// SPBU points
spbu.forEach((p, i) => {
const px = p.x * W, py = p.y * H;
const pulse = Math.sin(t * 2 + i * 1.2) * 0.4 + 0.6;
ctx.beginPath();
ctx.arc(px, py, 7 * pulse, 0, Math.PI * 2);
ctx.fillStyle = spbu24[i] ? 'rgba(245,197,66,0.15)' : 'rgba(245,130,66,0.15)';
ctx.fill();
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI * 2);
ctx.fillStyle = spbu24[i] ? '#F5C542' : '#F58242';
ctx.fill();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})();
// Card 2: Choropleth hexagons
(function () {
const cv = document.getElementById('c2');
const rect = cv.parentElement.getBoundingClientRect();
cv.width = rect.width || 360;
cv.height = 220;
const ctx = cv.getContext('2d');
let t = 0;
const kecamatans = [
{ x: 0.22, y: 0.35, w: 0.28, h: 0.32, density: 0.9, name: 'Pontianak Kota' },
{ x: 0.52, y: 0.35, w: 0.3, h: 0.28, density: 0.65, name: 'Pontianak Timur' },
{ x: 0.2, y: 0.62, w: 0.26, h: 0.28, density: 0.5, name: 'Pontianak Selatan' },
{ x: 0.5, y: 0.6, w: 0.3, h: 0.3, density: 0.3, name: 'Pontianak Barat' },
{ x: 0.2, y: 0.1, w: 0.32, h: 0.25, density: 0.75, name: 'Pontianak Utara' },
{ x: 0.55, y: 0.1, w: 0.28, h: 0.25, density: 0.45, name: 'Pontianak Tenggara' },
];
function densityColor(d) {
const r = Math.round(30 + d * 30);
const g = Math.round(80 - d * 60);
const b = Math.round(180 - d * 120);
return `rgba(${r},${g},${b},`;
}
function draw(ts) {
t = ts * 0.001;
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
kecamatans.forEach((k, i) => {
const pulse = 0.7 + Math.sin(t * 0.8 + i * 1.1) * 0.1;
const x = k.x * W, y = k.y * H, w = k.w * W, h = k.h * H;
const alpha = k.density * pulse;
ctx.fillStyle = densityColor(k.density) + alpha + ')';
ctx.strokeStyle = 'rgba(147,197,253,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.fill();
ctx.stroke();
});
// Legend bar
const lgx = W * 0.05, lgy = H * 0.87, lgw = W * 0.35, lgh = 8;
const grad = ctx.createLinearGradient(lgx, 0, lgx + lgw, 0);
grad.addColorStop(0, 'rgba(60,160,255,0.4)');
grad.addColorStop(1, 'rgba(20,30,140,0.9)');
ctx.fillStyle = grad;
ctx.beginPath(); ctx.rect(lgx, lgy, lgw, lgh); ctx.fill();
ctx.fillStyle = 'rgba(200,220,255,0.5)';
ctx.font = '8px DM Mono, monospace';
ctx.fillText('Rendah', lgx, lgy + 20);
ctx.fillText('Tinggi', lgx + lgw - 26, lgy + 20);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})();
// Card 3: Social aid — circles and connecting lines
(function () {
const cv = document.getElementById('c3');
const rect = cv.parentElement.getBoundingClientRect();
cv.width = rect.width || 360;
cv.height = 220;
const ctx = cv.getContext('2d');
let t = 0;
const centers = [{ x: 0.3, y: 0.38, r: 0.22 }, { x: 0.72, y: 0.55, r: 0.18 }];
const houses = [
{ x: 0.15, y: 0.25 }, { x: 0.35, y: 0.2 }, { x: 0.2, y: 0.55 }, { x: 0.4, y: 0.7 },
{ x: 0.6, y: 0.3 }, { x: 0.85, y: 0.35 }, { x: 0.75, y: 0.72 }, { x: 0.9, y: 0.65 },
{ x: 0.5, y: 0.5 }, { x: 0.12, y: 0.75 },
];
function nearestCenter(h) {
let best = 0, bd = 999;
centers.forEach((c, i) => {
const d = Math.hypot(h.x - c.x, h.y - c.y);
if (d < bd) { bd = d; best = i; }
});
return best;
}
function draw(ts) {
t = ts * 0.001;
const W = cv.width, H = cv.height;
ctx.clearRect(0, 0, W, H);
// Radius circles
centers.forEach((c, i) => {
const pulse = Math.sin(t * 1.2 + i * 1.5) * 0.03;
const R = (c.r + pulse) * Math.min(W, H);
ctx.beginPath();
ctx.arc(c.x * W, c.y * H, R, 0, Math.PI * 2);
ctx.fillStyle = i === 0 ? 'rgba(232,121,249,0.06)' : 'rgba(168,85,247,0.08)';
ctx.fill();
ctx.strokeStyle = i === 0 ? 'rgba(232,121,249,0.3)' : 'rgba(168,85,247,0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
});
// Connection lines
houses.forEach((h, i) => {
const ci = nearestCenter(h);
const c = centers[ci];
const alpha = 0.15 + Math.sin(t + i) * 0.08;
ctx.beginPath();
ctx.moveTo(h.x * W, h.y * H);
ctx.lineTo(c.x * W, c.y * H);
ctx.strokeStyle = ci === 0 ? `rgba(232,121,249,${alpha})` : `rgba(168,85,247,${alpha})`;
ctx.lineWidth = 0.8;
ctx.stroke();
});
// Houses
houses.forEach((h, i) => {
const ci = nearestCenter(h);
ctx.beginPath();
ctx.arc(h.x * W, h.y * H, 3, 0, Math.PI * 2);
ctx.fillStyle = ci === 0 ? 'rgba(232,121,249,0.8)' : 'rgba(168,85,247,0.8)';
ctx.fill();
});
// Centers (mosques/churches)
centers.forEach((c, i) => {
const pulse = 1 + Math.sin(t * 1.5 + i) * 0.15;
ctx.beginPath();
ctx.arc(c.x * W, c.y * H, 6 * pulse, 0, Math.PI * 2);
ctx.fillStyle = i === 0 ? 'rgba(232,121,249,0.2)' : 'rgba(168,85,247,0.2)';
ctx.fill();
ctx.beginPath();
ctx.arc(c.x * W, c.y * H, 4, 0, Math.PI * 2);
ctx.fillStyle = i === 0 ? '#E879F9' : '#A855F7';
ctx.fill();
});
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})();
/* ─── INTERSECTION OBSERVER ─── */
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('visible');
});
}, { threshold: 0.15 });
document.querySelectorAll('.fade-up').forEach(el => observer.observe(el));
</script>
</body>
</html>