{# ============================================================ FIXED BACKDROP -- never scrolls, fills viewport ============================================================ #}
{# ============================================================ SCROLLING CONTENT -- overlays the fixed backdrop ============================================================ #}

Building a Construction Paper Diorama Website with Django

Mar 2026 Technical

Most of the software I build is SaaS: database-backed CRUD applications with forms, dashboards, and Stripe integration. Useful, but not exactly a playground for creative expression. When I decided to build my personal site, I wanted to do something different. No templates, no component libraries, no framework CSS. Instead, every page would be a hand-cut construction paper diorama that changes with the time of day.

This article covers the technical details of building onehitekrednek.com: a real-time time-of-day engine, procedural tree generation, SVG scene composition, and the CSS techniques that make flat shapes feel like layered paper.

The idea

The concept started with a simple question: what if a personal website felt like opening a pop-up book? Every page would be a complete scene with mountains, a lake, trees, clouds drifting across the sky, the sun tracking an arc overhead. And the whole thing would shift colors based on what time it actually is where you are. Visit at dawn and the sky is peach and coral. Come back at midnight and you get deep indigo with stars and a moon that shows the correct lunar phase.

The constraint I set for myself: no JavaScript frameworks, no CSS frameworks, no build tools beyond what Django provides. Just Django templates, custom CSS, vanilla JavaScript, and hand-drawn SVG paths.

The time-of-day engine

The centerpiece of the site is a client-side JavaScript engine that calculates the visitor's local time and smoothly interpolates between six color phases:

Phase Hours Character
Night 9 PM -- 5 AM Deep indigo sky, stars visible, moon with real lunar phase
Dawn 5 AM -- 6 AM Peach and coral horizon, warm orange sun
Morning 6 AM -- 12 PM Classic blue sky, bright greens
Afternoon 12 PM -- 6 PM Deeper blue, slightly muted tones
Sunset 6 PM -- 7 PM Red and orange drama
Dusk 7 PM -- 9 PM Purple and violet fade

Each phase defines a complete color set, not just the sky, but the mountains, lake, grass, clouds, card backgrounds, text colors, and accent tones. That is over 25 CSS custom properties per phase, all interpolated smoothly as the clock moves between phases.

The interpolation converts hex colors to RGB, lerps each channel, and converts back:

function lerpColor(c1, c2, t) {
    var a = hexToRgb(c1);
    var b = hexToRgb(c2);
    return rgbToHex(
        a[0] + (b[0] - a[0]) * t,
        a[1] + (b[1] - a[1]) * t,
        a[2] + (b[2] - a[2]) * t
    );
}

The engine updates every minute in production, but during development a scrubber slider lets you drag through the full 24-hour cycle. A ?time=HHMM query parameter locks the scene to a specific time for testing or screenshots.

Celestial mechanics

The sun follows a sine-curve arc from 6:00 AM to 8:30 PM, fading in and out near the horizon. The moon rises at 7:00 PM and sets at 7:00 AM, following a similar arc.

The moon phase is calculated from the Julian Date using the synodic cycle (29.53 days). The shadow overlay width is derived from the illumination fraction, and its position (left or right) depends on whether the moon is waxing or waning. This means the moon on the site matches the actual moon outside your window.

Stars are 50 randomly positioned divs in the sky area, each with a randomized twinkle animation. They fade in at dusk and out at dawn using calculated opacity ramps.

The time scrubber

The time scrubber started as a development tool. When you are tuning 25 color properties across six phases, you need a way to see all of them without waiting for actual sunrise. So I added a range input that maps 0-1439 (minutes in a day) to the applyTimeOfDay function. Drag it and the entire scene updates instantly, including the sky gradient, mountain fills, cloud tones, card backgrounds, lake color, sun position, moon position, star opacity, everything.

I built it expecting to hide it behind a debug flag before launch. But once it was there, I kept using it just to watch the transitions. Dragging from dawn through morning into afternoon and then pulling it into sunset is genuinely satisfying. The smooth color interpolation means there are no hard cuts, you can park the slider at 5:47 AM and get a sky color that exists only in that one-minute window between deep dawn orange and early morning blue.

It also turned out to be the best way to show the site to someone. Instead of saying "come back at 10 PM to see the night mode," you hand them the slider and let them scrub through the full cycle in five seconds. It communicates the entire concept faster than any explanation.

So I left it in. It sits in the corner of every page, small and unobtrusive. A ?time=HHMM query parameter also locks the scene to a specific minute for screenshots or sharing a particular mood. The scrubber was a dev tool that became a feature because it made the site more fun to interact with.

Per-page scene variants

Every page is a complete diorama, not a shared background with different content pasted on top. Each page defines its own:

  • Mountain silhouettes (different peak shapes, heights, and layering)
  • Lake shape (different shoreline contours)
  • Tree layout (procedurally generated with different seeds per page)
  • Grass and flower positions

The scene variant is set via data-scene on the body element. The scenery engine reads this attribute and uses it as a seed for procedural generation, so the same page always produces the same tree layout, but different pages get different forests.

Procedural tree generation

Trees are built entirely in JavaScript at page load. The createTreeSVG function generates each tree as an inline SVG with a rectangular trunk and a tiered polygon foliage shape. Tree heights range from 4px (tiny distant silhouettes on the hilltops) to 95px (large foreground pines).

The generation runs in five bands, each representing a depth layer:

  1. Hilltop band (y: -18% to -5%): Extremely tiny trees (4-11px) planted up in the mountain zone
  2. Treeline band (y: -8% to 5%): Very small trees (6-18px) at the hill-terrain boundary
  3. Far band (y: 0% to 18%): Small trees (10-26px), densely packed
  4. Mid band (y: 8% to 30%): Medium trees (18-40px)
  5. Foreground band (y: 30% to 70%): Large trees (50-95px), sparse

Every tree placement checks whether it would overlap the lake using an elliptical exclusion zone. The exclusion uses the actual DOM bounding box of the lake SVG element, so it adapts to different lake shapes across page variants.

A seeded pseudorandom number generator ensures deterministic layouts. The seed is derived from the page's data-scene attribute by summing character codes, so "landing" and "blog" always produce different but stable forests.

The construction paper aesthetic

The visual identity comes from CSS clip-path: polygon() applied to every card, button, pill, tag, and label on the site. Instead of rounded corners, elements have irregular edges that simulate hand-cut paper:

.paper-cut {
    clip-path: polygon(
        1% 4%, 3% 0%, 8% 2%, 15% 0%, 22% 3%, 30% 1%,
        38% 0%, 45% 2%, 52% 0%, 60% 1%, 68% 0%, 75% 3%,
        82% 1%, 90% 0%, 95% 2%, 98% 0%, 100% 3%,
        100% 6%, 99% 15%, 100% 25%, ...
    );
}

Each polygon traces an irregular path around the perimeter with small vertical and horizontal wobbles. Different elements use different polygons so they do not all share the same cut pattern. Nth-child selectors apply alternate polygons to siblings, preventing visual repetition in lists.

Every interactive element also gets a slight CSS rotation (typically 0.2-0.5 degrees, alternating positive and negative) and a dual box-shadow with both an outer drop shadow and an inner paper-grain highlight. On hover, elements translate upward a few pixels and rotate slightly further, as if being picked up off the page.

Cloud navigation

The navigation bar is made of individual cloud shapes. Each nav item is an anchor wrapping a unique SVG cloud path with a label positioned absolutely at its center. The clouds use clamp() for responsive sizing and have a gentle bobbing animation with staggered delays so they drift independently.

Isometric depth

The content area uses CSS perspective to create a subtle diorama tilt:

.content-area {
    perspective: 800px;
}
.iso-content {
    transform: rotateX(1.5deg);
    transform-origin: center top;
}

This gives cards a slight receding-into-the-scene quality without distorting text readability. Combined with the paper-cut edges and slight rotations, it creates the feeling of looking down into a shoebox diorama.

Dynamic lake reflections

The lake is not just a colored SVG shape. A reflection system clips blurred ellipses to the lake's path and tracks the positions of the sun, moon, and stars in real time.

The sun reflection is a blurred horizontal ellipse whose x-position maps to the sun's screen position relative to the lake. Its opacity scales with the sun's current visibility. At night, the moon reflection takes over, and faint star reflections dot the water surface. All of this runs in a requestAnimationFrame loop.

The manage interface

Instead of relying solely on Django's admin, the site has a custom management area at /manage/ that lives inside the diorama. Every manage page gets the same scene treatment for mountains, lake, trees, construction paper cards. You can edit site settings, manage blog posts, create user accounts, and publish per-user content, all without leaving the paper world.

The per-user content system replaces a traditional family-gate password with individual accounts. Each person gets their own login and sees only the content curated for them with text posts, photo galleries, file attachments, or annotated links. The superuser creates accounts with auto-generated passwords and assigns content through a multi-select interface.

The stack

The whole site runs on what I call SnowyStack:

  • Django for routing, templates, ORM, auth, and the management interface
  • HTMX for blog category filtering, pagination, and inline form interactions
  • SQLite as the database (this is a personal site, not a high-traffic SaaS)
  • Custom CSS for everything visual, no Tailwind, no Bootstrap, no component library
  • Vanilla JavaScript for the time engine, scenery generation, and lake reflections
  • WhiteNoise for static file serving
  • Gunicorn behind Caddy for production

Total JavaScript on the site: two files. time-engine.js handles the 24-hour color cycle, celestial positioning, and stars. scenery.js handles procedural tree generation, cloud animation sync, and lake reflections. Everything else is server-rendered HTML and CSS.

What made it fun

Building business applications, there are clear right answers. You validate the form, save to the database, redirect with a success message. The construction paper site had none of that. How irregular should the paper-cut edges be? How fast should clouds drift? How much should a tree lean in the breeze? Every decision was aesthetic, not functional.

The time-of-day engine was the most satisfying piece. Watching the sky shift from dawn coral to afternoon blue to sunset orange while the moon shadow tracks the actual lunar cycle. That is the kind of thing that has no business value whatsoever and is exactly why I built it.

Building a personal site should be personal. This one looks like a diorama because I have always liked the way construction paper looks. It tracks the real moon because I think that is interesting. Every page has different mountains because a portfolio site with one static background felt lazy. None of these choices would survive a product sprint, and that is the point.