self

writing about this website.

Table of Contents

  1. Loading Effect
  2. Profile Image
  3. Navigation
  4. Projects
  5. Gallery
  6. Devlogs

April 10th, 2017
writing on April 17th, 2017

Loading Effect

When you first hit the webpage, the intended effect is the webside creating itself. What is actually happening is styles are being loaded onto the page dynamically. The page starts out with only the following styles, which hide most of the elements on the page.

.projects.projects, .gallery, .navigation, .links, .name, .label, img, svg, li { display: none }

Upon loading the page, this javascript is executed. It loads styles.css into memory, then creates a style element and pre element, which is used to display code. It puts both these elements on the page, then adds text from styles.css to both of them line by line. This has the effect of changing the styles on the page at the same time that they show up on screen.

fetch("/styles.css").then(r => r.text()).then(text => {
  const lines = text.split('\n');
  const len = lines.length;
  const style = document.createElement('style');
  document.head.appendChild(style);
  const pre = document.createElement('pre');
  document.getElementsByClassName('container')[0].appendChild(pre);

  const write = (ls) => {
    if (ls.length === 0) {
      document.getElementById('pre').remove()
    } else {
      const l = ls.shift() + "\n";
      style.textContent += l; 
      pre.textContent += l;

We use this timing function to add lines faster and faster to the screen. Eventually, there’s a style rule that hides the pre element. However, we only do all this on the first time the user hits the page. Immediately after hitting the page, you might notice that they URL changes to ‘/home’. This will be explained later…

      setTimeout(() => write(ls), 250/Math.floor((len - ls.length)/3));
    }
  };
  (window.location.pathname === "/" ? () => write(lines) : () => {style.textContent = lines.join("\n"); write([]);})();
  nav(window.location.pathname.substring(1) || "home");
});

Profile Image

The profile image is animated using an svg polygon element and the stroke-dasharray and stroke-dashoffset properties. reference.

The three pages on the page are just different divs called home, gallery, projects. Navigation between them is just done by setting styles for them to be shown or not. There are symlinks for home.html, gallery.html, and proejcts.html that point to index.html, so if you revisit the page or pass around the URL, this should still work.

const nav = (page) => {
  window.history.pushState({}, page, page);
  ['home', 'gallery', 'projects'].map(c => 
    document.getElementsByClassName(c)[0].className = c + (c === page ? " active" : " hidden")
  );
}

Projects

Projects are just loaded in dynamiaclly from a file called projects.json.

Note, the following code on this page is pretty ugly.

fetch("/projects.json").then(r => r.json()).then(data => {
  const projects = document.getElementsByTagName('ol')[0];
  data.map(p => {
    const el = document.createElement('li');
    el.innerHTML = '<a href="'+p.link+'" '+(p.link[0] === '/' ? '' : 'target="_blank"')+'><div class="title">'+p.title+'</div><p class="desc">'+p.desc+'</p><div class="pos">'+p.alt+'</div></a>';
    projects.appendChild(el);
  });
});

The gallery is implemented in a similar way to projects, execept the structure is little more involved. Like before, we load a file gallery.json which contains filenames for all the photos and their description, split up by section. We create labels for each section, which are just horizontally scrolling divs.

fetch("/gallery.json").then(r => r.json()).then(data => {
  const albums = document.getElementsByClassName('albums')[0];
  Object.keys(data).map((k,i) => {
    const wrap = document.createElement('div');
    wrap.innerHTML = '<div class="label">'+k+'</div>';

I can’t seem to find the page where I found this trick, but you can use radio style inputs to create a css-only gallery. CSS allows you to select adjacent siblings, so we put a radio input before each image. When that radio button is checked, we can show the li elem that directly follows it.

Then, we set onclick handlers on each image to check their corresponding radio input. Each image also has previous and next buttons, which will check the radio buttons for the previous and next images, respectively.

There’s this annoying CSS quick that I’ve ran into oh so many times in the past. You can’t have overflow scrollable on one axis and visible on the other (I believe). This manifests here because when we want to show the full image, we can’t escape the bounds of the smaller horizontal album. The solution–you can explore styles.css–is to just move everything else out of the way and make the selected album take up the entire screen. This state is marked by the class fullscreen. Annoying, but works.

The span is placed around the img to allow us to place the description for the iamge below each image. The description is normally hidden, and is only shown in the fullscreen state.

    const list = document.createElement('ul');
    data[k].map((f,j) => {
      const btn = document.createElement('div');
      btn.innerHTML = '<input type="radio" name="fullscreen" id="'+(i+'.'+j)+'"/>';
      const item = document.createElement('li');
      item.innerHTML = ('<span><img src="'+("/gallery/"+k+"/"+f.file)+'"/><span>'+f.desc+'</span></span>'+
        (j > 0 ? '<label class="prev" for="'+(i+'.'+(j-1))+'">&#x2039;</label>' : '')+
        (j < data[k].length - 1 ? '<label class="next" for="'+(i+'.'+(j+1))+'">&#x203a;</label>' : ''));
      const input = btn.firstChild;
      input.onchange = () => albums.className = "albums fullscreen";
      item.firstChild.onclick = () => input.click();
      list.appendChild(input);
      list.appendChild(item);
    })
    wrap.appendChild(list);
    albums.appendChild(wrap);
  });
  document.getElementsByClassName('close')[0].onclick = () => albums.className = "albums";

It’s quite annoying to keep clicking the left and right buttons, so we add a keypress handler and catch left and right kepyresses. We get the index of the currently selected image, and if the image that we are intending to go to exists, we click that image’s radio input.

  document.onkeydown = (e) => {
    if (e.keyCode !== 37 && e.keyCode !== 39) return;
    const sp = document.querySelector('input:checked').id.split('.');
    const el = document.getElementById(sp[0]+'.'+(parseInt(sp[1]) + (e.keyCode === 37 ? -1 : 1)));
    if (el) el.click();
  };
});

Devlogs

Because I wanted to write devlogs like this one, I brought the whole project into jekyll. This lets me write my logs in markdown, which jekyll converts directly to html. I bring in styles as necessary to format the code blocks and images on these pages.

Note: clean this up when you have time.