358 lines
9.0 KiB
Vue
358 lines
9.0 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<Transition name="modal">
|
||
<div v-if="isOpen" class="modal-backdrop" @click="closeModal">
|
||
<div class="modal-content" @click.stop>
|
||
<button class="close-button" @click="closeModal" aria-label="Close">
|
||
×
|
||
</button>
|
||
|
||
<div class="modal-body">
|
||
<h2>About This Project</h2>
|
||
|
||
<section class="intro">
|
||
<p>
|
||
Around 2,000 years ago, the Hopewell culture built a 60-mile ceremonial road
|
||
connecting Newark to Chillicothe, Ohio. Most of it has been destroyed by farming,
|
||
but tiny remnants might still be hiding in the landscape—parallel earthen walls
|
||
just 50cm tall, visible only as subtle shadows in the right light.
|
||
</p>
|
||
<p>
|
||
There's way too much terrain for archaeologists to analyze alone. That's where
|
||
you come in! This tool lets you explore high-resolution lidar data and help
|
||
search for lost sections of the Great Hopewell Road.
|
||
</p>
|
||
</section>
|
||
|
||
<section class="video-section">
|
||
<h3>Learn More About the Road</h3>
|
||
<div class="video-container">
|
||
<iframe
|
||
src="https://www.youtube.com/embed/Ltu2hJwqId8"
|
||
title="The Great Hopewell Road"
|
||
frameborder="0"
|
||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||
allowfullscreen
|
||
></iframe>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="instructions">
|
||
<h3>How to Use This Tool</h3>
|
||
|
||
<p>
|
||
<strong>Start exploring:</strong> Check out the known archaeological sites on the map.
|
||
Click the pin icon next to each site name to fly there and see the crisp lidar imagery.
|
||
Click on any of the pins on the map to learn more about the cite, and follow up on
|
||
the cited references to dive further down the rabbit hole.
|
||
Use the sidebar controls to customize what you see.
|
||
</p>
|
||
|
||
<p>
|
||
<strong>Hunt for the road:</strong> Right-click anywhere on the map (within Ohio) to
|
||
request a tile. If someone's already requested it, you can load the interactive data
|
||
instantly. Otherwise, you'll wait a bit for the processing pipeline.
|
||
</p>
|
||
|
||
<p>
|
||
<strong>Adjust the lighting:</strong> Once a tile loads, the shading sandbox opens.
|
||
Change the lighting direction and exaggerate the terrain to increase contrast—this
|
||
makes subtle features pop out. Click "Render Tile" to add it to the map.
|
||
</p>
|
||
|
||
<p>
|
||
<strong>Mark your finds:</strong> Found something interesting? Drop a pin using the
|
||
geometry tools in the top right. Then share it on
|
||
<a href="http://discord.gg/miniminuteclan" target="_blank" rel="noopener noreferrer">
|
||
Discord
|
||
</a>
|
||
with coordinates and a screenshot!
|
||
</p>
|
||
</section>
|
||
|
||
<section class="about-author">
|
||
<h3>About me</h3>
|
||
<p>
|
||
I'm Mark Kalsbeek, a web developer and researcher from the Netherlands.
|
||
I was inspired to build this after watching Milo's video above. If you want to
|
||
reach me, @ me on
|
||
<a href="http://discord.gg/miniminuteclan" target="_blank" rel="noopener noreferrer">
|
||
Milo's Discord
|
||
</a>:
|
||
<span v-if="!usernameRevealed" class="username-reveal" @click="revealUsername">
|
||
reveal
|
||
</span>
|
||
<span v-else class="username">{{ username }}</span>
|
||
</p>
|
||
</section>
|
||
|
||
<section class="thanks">
|
||
<h3>Thanks</h3>
|
||
<p>
|
||
Special thanks to <a href="https://gis1.oit.ohio.gov/geodatadownload/" target="_blank" rel="noopener noreferrer">Ohio OGRIP</a>
|
||
for making high-quality lidar data freely available with an easy-to-reverse API.
|
||
</p>
|
||
<p>
|
||
Map tiles from <a href="https://www.openstreetmap.org/" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors
|
||
and satellite imagery from <a href="https://www.esri.com/" target="_blank" rel="noopener noreferrer">Esri</a>.
|
||
</p>
|
||
<p class="tech-stack">
|
||
Built with: Elixir (plug_cowboy, jason, geo, logger_file_backend, httpoison, mime),
|
||
Python (laspy, scipy, numpy, pyproj),
|
||
Deno (Vue, Vite, Three.js, MapLibre GL, Pinia),
|
||
and of course Claude for infinite amounts of grunt work.
|
||
</p>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, watch, onMounted } from 'vue';
|
||
|
||
const MODAL_VERSION = '1.0';
|
||
const STORAGE_KEY = 'aboutModalVersion';
|
||
const OBFUSCATED_USERNAME = '404d61726b6b313136';
|
||
|
||
const isOpen = ref(false);
|
||
const usernameRevealed = ref(false);
|
||
const username = ref('');
|
||
const seenRecentVersion = ref(true);
|
||
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
});
|
||
|
||
const revealUsername = () => {
|
||
username.value = OBFUSCATED_USERNAME.match(/.{2}/g)
|
||
.map(hex => String.fromCharCode(parseInt(hex, 16)))
|
||
.join('');
|
||
usernameRevealed.value = true;
|
||
};
|
||
|
||
watch(() => props.modelValue, (newVal) => {
|
||
isOpen.value = newVal;
|
||
});
|
||
|
||
watch(isOpen, (newVal) => {
|
||
emit('update:modelValue', newVal);
|
||
if (!newVal) {
|
||
// Mark as seen when closed
|
||
localStorage.setItem(STORAGE_KEY, MODAL_VERSION);
|
||
seenRecentVersion.value = true;
|
||
}
|
||
});
|
||
|
||
const closeModal = () => {
|
||
isOpen.value = false;
|
||
};
|
||
|
||
// Check if should show on mount
|
||
onMounted(() => {
|
||
const seenVersion = localStorage.getItem(STORAGE_KEY);
|
||
if (seenVersion !== MODAL_VERSION) {
|
||
seenRecentVersion.value = false;
|
||
}
|
||
});
|
||
|
||
// Expose methods for parent component
|
||
defineExpose({
|
||
open: () => { isOpen.value = true; },
|
||
seenRecentVersion,
|
||
close: closeModal
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.modal-backdrop {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
max-width: 60vw;
|
||
max-height: 80vh;
|
||
width: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.modal-content {
|
||
max-width: 95vw;
|
||
max-height: 90vh;
|
||
}
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
background: none;
|
||
border: none;
|
||
font-size: 2rem;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 0;
|
||
width: 2rem;
|
||
height: 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: color 0.2s;
|
||
z-index: 1;
|
||
}
|
||
|
||
.close-button:hover {
|
||
color: #000;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 2rem;
|
||
padding-top: 3rem;
|
||
}
|
||
|
||
h2 {
|
||
margin-top: 0;
|
||
margin-bottom: 1.5rem;
|
||
font-size: 1.8rem;
|
||
color: #333;
|
||
}
|
||
|
||
h3 {
|
||
margin-top: 2rem;
|
||
margin-bottom: 1rem;
|
||
font-size: 1.3rem;
|
||
color: #444;
|
||
}
|
||
|
||
section {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
section:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
p {
|
||
line-height: 1.6;
|
||
color: #555;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
p:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.intro p:first-child {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.video-section {
|
||
margin: 2rem 0;
|
||
}
|
||
|
||
.video-container {
|
||
position: relative;
|
||
width: 100%;
|
||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||
margin: 1rem 0;
|
||
background: #000;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.video-container iframe {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.instructions p {
|
||
margin-bottom: 1.2rem;
|
||
}
|
||
|
||
.instructions strong {
|
||
color: #333;
|
||
}
|
||
|
||
a {
|
||
color: #0066cc;
|
||
text-decoration: none;
|
||
}
|
||
|
||
a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.username-reveal {
|
||
display: inline-block;
|
||
background: #333;
|
||
color: white;
|
||
padding: 0.1rem 0.5rem;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
user-select: none;
|
||
}
|
||
|
||
.username-reveal:hover {
|
||
background: #444;
|
||
}
|
||
|
||
.username {
|
||
font-family: monospace;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tech-stack {
|
||
font-size: 0.85rem;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Modal transitions */
|
||
.modal-enter-active,
|
||
.modal-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.modal-enter-from,
|
||
.modal-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.modal-enter-active .modal-content,
|
||
.modal-leave-active .modal-content {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.modal-enter-from .modal-content,
|
||
.modal-leave-to .modal-content {
|
||
transform: scale(0.9);
|
||
}
|
||
</style> |