965 lines
32 KiB
HTML
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> |