Downloading the entire CAD-comic archive with Python

Over the weekend I had some time to play around with Python. I had not touched Python for the last 7-8 years, and even then had only briefly used it. Though I am not a fan of some parts of the language, the speed with which you can whip something together is actually quite impressive.

Even though I had almost no experience with the language, in about two hours I had a script that could download all CTRL+ALT+DEL-comics given a year, or a range of years.

The complete code is available on my github. I will just explain the general idea that I had.

First of all, the archive is available on and you can choose which year you want. Each year just changes the URL by appending the year to the archive endpoint, for example for 2013:

On this page, you find a list of all the comics that have been published that year, with a url to refer to them. I had to grab the URLs for each comic for a given year. Afterwards, when I had the URL I had to fetch the image that was displayed on that page. By looking at the source, I determined that I could find it under the _content_ div, and then just the only image.

Using BeautifulSoup, scraping a website is easy and fast. In addition, the library urllib2 for Python makes sending web requests easy as well.

Let's take the code apart now.

Before we begin with the actual code, an important step is to set up our headers for the requests we will be making over the web. If we do not do this, then our web requests will fail as they will be blocked by the server that CAD-comics is running on.

# Pass some headers, so the website allows our requests.
hdr = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
       'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
       'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3',
       'Accept-Encoding': 'none',
       'Accept-Language': 'en-US,en;q=0.8',
       'Connection' : 'keep-alive'}

First we have to scrape the archive for a given year. Since we know how to build the URL for this year, we just have to send a request to that, and use BeautifulSoup to fetch all the hrefs on the page. In addition, I want to filter the hrefs to only refer to pages that have a comic. Otherwise I would have hrefs to other parts, such as to social media. I figured that all the comic urls also have the year in them, as comic urls have their published date in them, thus my filter can just look for the year in the href string. This should limit the errors we make.

""" Scrape an archive for a given year, finds retrieves all the comic URLs for this year."""
def scrapeArchiveForComics(year):
    print year
    global scrapingYear
    global index
    index = 0 # We set the index to 0 again, because we are creating a new list of comics
    scrapingYear = year
    archiveUrl = "" + year

    # Look for all the URLs (a-tags) containing '2002'
    request = urllib2.Request(archiveUrl,headers=hdr);
    req = urllib2.urlopen(request)
    soup = BeautifulSoup(,'html.parser')
    aTags = soup.find_all("a")
    comicTags = filter(urlContainsYear,aTags)
    comicUrls = map(mapFetchHrefFromImageUrl,comicTags)
    # These are sorted by how they appear in the archive. They need to be reversed (last on archive = first chronologically)
    return (list(reversed(comicUrls)))

For filtering them, I did not do it inline (I'm sure you can do this in Python with a Lambda expression, and it would probably be more elegant. In fact, I am sure that a lot of this python code will make seasoned pythonistas want to smack me with a book on Python.

Anyway, I made a global of the year that I am scraping, so in my urlContainsYear method, I can just look for this in the url.

def urlContainsYear(url):
    global scrapingYear
    stringUrl = str(url) # Because BeautifulSoup makes it a 'tag' normally
    return scrapingYear in stringUrl

To get the actual href from the BeautifulSoup tag elements, I used a map function. Once again, this should probably be a lambda.

def mapFetchHrefFromImageUrl(imgUrl):
    return str(imgUrl["href"])

This method returns me urls like this: /cad/20120123, /cad/20120213,..

Now that I have these urls, I can build them by joining them with the baseurl and then looking for the image in the resulting HTML. For this, I have used another method that I call for each returned url.

def scrape(partialComicURL):
    global scrapingYear
    global index
    print "Scraping CAD-comics.."
    site = baseUrl + partialComicURL
    request = urllib2.Request(site,headers=hdr)
    f = urllib2.urlopen(request)
    soup = BeautifulSoup(,'html.parser')

    # Find the image source
    contentDiv = soup.find(id="content")
    imageSource = contentDiv.find_all('img')[0]["src"] # src attribute of first element in the array (only one result for the URL)
    imageRequest = urllib2.Request(imageSource,headers=hdr)

    # format filename, jus take the last part of the comic (after 2nd slash of partial)
    extension = imageSource[-3:]
    filename = str(index) + " : " + (partialComicURL.split("/")[2]) + "." + extension

    #Write the image to a file
    f = open(scrapingYear+"/"+filename,"wb")

I believe this code is quite self-explanatory to anyone familiar with Python. I do keep an index, which might look odd. But the index is so that I can prefix the filename with a number, to ensure that they are in the right order when viewing them locally. Order can be quite important for the CAD-Comic, as some storylines run over multiple comics and they need to be in order. That's also the reason why in the end of the scrapeArchiveForComics method, I reverse the list. Because from the website, it comes in 'most-recent' first. The reverse order of what we actually want.

In addition, I also format the filename a bit more. I just use the date of the comic, and I look for the extension in the image source to determine whether it is .jpg or .png. At some point in time, Tim (author of CAD), decided to change image format it turns out.

Those are actually the two most interesting methods that we need to make this work. The main method I wrote takes some CLI arguments to fill in the parameters for these methods.

def main():
    if len (sys.argv) > 1:
        year = sys.argv[1]
        if year == "all":
            startYear = int(sys.argv[2]) if len (sys.argv) == 3 else 2002
            print "Scraping for all years"
            thisYear =
            yearRange = range(startYear,thisYear+1) # +1 to include the current year. Otherwise range is not-inclusive
            for archiveYear in yearRange:
            print "Done!"
            print "Scraping for year: " + year
            print "Done!"
        print "Pass a year, starting from 2002 (sample usage: python 2002"

This will call a 'downloadForYear' method, which in addition to starting the scraping, will also create a folder to store the images.

""" main method to download for year"""
def downloadForYear(year):
    comicUrls = scrapeArchiveForComics(year)
        os.mkdir(year) # Create a folder to store the comics.
    except OSError:
            pass # the folder already exists, should we maybe empty it?

    i = 0
    for comic in comicUrls:
        print "Scraping CAD-comic #" + str(i) + " from: " + str(len(comicUrls)) + " for year: " + year

That is pretty much the whole code. I did omit some parts of the code here, as the full source is available on github as mentioned earlier. So make sure to check that out if you have any issues getting this to run. Plus, if you want to fix things about this code, PRs are always welcome! 🙂

I must say that even though the language and the syntax of Python might look a bit alien to me (coming from a background with Java and Haskell), the amount of work you can do with so much ease is appealing to me. In fact, I am sure that I will end up writing some more small Python scripts in the future, as I had a great deal of fun playing around with the language.

Bye bye 2016, Hello 2017

A new year is upon us! This year was a pretty damn great year and I will once again write a small update on what is going on with me in a blogpost. As has become quite obvious, I have written very few blogposts in the past year, in fact it is getting less and less each year so I fear that this might be the only blogpost of what is now just 2017 for quite some time.

2016 was a pretty damn great year, I got married which is of course a major highlight in ones life, but I also started working in the University Hospital of Leuven as a software engineer writing Java code.

I've never before made new years resolution, but this year I will try to give it a shot anyway. As it is hard to predict beforehand how a year will turn out, these things are as always susceptible to change but I will try to follow it as much as I can, and I will just make rather modest claims and will limit myself to two concrete goals.

First of all, I want to improve my knowledge of Haskell. I am not starting from scratch here, I picked up the well-known LYAH (Learn yourself a Haskell for great good) around the summer of 2016 and through a rather busy shedule (planning a wedding, new job, ..) managed to finish about half of it and write some small programs in Haskell, mostly solving problems from ProjectEuler.

As to how I will improve my Haskell knowledge, it mostly consists of finishing LYAH and continuing with "real world haskell" or another book that I have yet to decide on. In addition, I spend some time mostly lurking in the #haskell irc group on freenode which is one of the most pleasant IRC chatrooms that I have ever encountered, with interesting discussions about computer science related topics and of course helpfull content for those learning Haskell.

My second claim is something that I have been trying to do for some time now but never properly got into. I would like to learn some machine learning, not something breakthrough but I would like to be able to say this time next year that I managed to write some small programs that make use of machine learning to solve some toy problems.

At this point I have no clue how much I will be able to learn, I just know that it is an interesting field of which I would like to know more. I'll pick up some books on the subject that I will try to go through.

Surely if something is interesting enough, I will blog about it. Yet it seems that I put most of my interesting projects on github nowadays. If you do want to stay more up to date with what I am doing, consider following me on github rather than on my blog.

Happy newyear!

Webtask, Wikipedia and Slack.

Whilst playing around with webtask I thought it would be a fun idea to create a little project which, when providing the webtask with a keyword, posts a message to slack containing the first paragraph of actual content from wikipedia.

Webtask is quite a fun technology to play around with. In essence, it is running a node-server remotely on which code is ran when an HTTP call is made. We do not set up the server ourselves, so in that regard you can think of it as a 'serverless' application. We only write the code, deploy it using webtask, and access it through our browser.

If you know how to program for node.js, you know how to program for Webtask. To find out which node modules are supported, there is a handy website. Browsing around here it did seem like most of the frequently used node modules, and with almost 1K modules at the time of writing, plenty is supported.

The goal of this project is rather simple, though it can be considered more of a 'PoC' rather than an actual implementation of this idea. I did not set up a slack bot for this, but you can assume that it is made for such bots. When a user would write something akin to

@wikibot Database

a request would be made to our webtask, which posts in the slack channel the first paragraph of wikipedia, with a link to the full article.

Webtask has a great guide on getting started, so if you have not done so yet I'd recommend reading it and following the steps outlined there. Once you have your first Webtask project set up, you're good to go.

We are going to require two modules for this.

  • request
  • cheerio

Request will be used to create a POST and GET request, to slack and wikipedia respectively. Cheerio will be used to parse the HTML content from the wikipedia page, and filter for the information we want to push to slack.

With this information we can write the GET request we need for wikipedia using both these modules.

function getWikiInfo(keyword)
    console.log("getting wikipedia information");
    var wikiURL = '' + keyword;

    console.log("fetching information from: " + wikiURL);

    request(wikiURL, function(error,response,body){
	if(!error && response.statusCode == 200){
	    var $ = cheerio.load(body);
	    var wikiInfo = $("#mw-content-text > p").first().text();

	    // we post this to slack, but add a link to the origin as well.
	    wikiInfo += " *... read more:* " + wikiURL.replace(' ', '%20');

Don't worry about the 'keyword' parameter at this point. This is passed with the Webtask URL we will use, which will become clear at the end of the post. As you can tell, we get some Keyword for wikipedia, which can be any valid wikipedia search. e.g: Donut, Homer Simpson or Dogs.

Next we look for the first paragraph in the HTML we get back using cheerio. Afterwards, we add some of our own content to this paragraph, which includes the slack markdown formatting so the '... read more: url' will be better visible inside slack.

When we have this information, we want to push it to slack. For slack, we need two extra variables. A slack URL, which is the URL for our slack webhook, and a username for our bot. These two are stored in slackURL and username respectively in my example. The method then looks like the following:

function slackPost(wikiInfo)
    var slack_data = {
	'username' : 'wiki bot'

    var opts = {
	method : 'POST',
	url : slackURL,
	headers : {
	    'Content-Type' : 'application/json'
	json : slack_data

    request(opts, function(error,response,body){
	    console.log("posted successfully");

We now have the two main methods, and we only need to put this together. Our webtask URL will contain an attribute for the wikipedia keyword we want to perform, so we'll need to fetch that out of the URL before calling our getWikiInfo method.

module.exports = function (context,cb) {
    var keyword =;
    // get wiki info and post to slack.
    cb(null, 'Posted information about ' + keyword + ' to slack');    

There we go, we can now run our webtask with `wt create filename.js`. This will give us a URL which we can navigate to in order to run our code. We do need to add the 'wiki' tag to get information about a topic which we want.

For example, I have tried this out with 'Homer Simpson' and 'Donut'.


Screen Shot 2016-08-01 at 10.11.15



The full code can be found on my github repository.

YouTube new subscriber overlay

{If you found this place because you are a YouTube livestreamer and would like to use this but you do not have any programming knowledge or a server, feel free to contact me at meeusdylan [at] hotmail [dot] com.}

As people who watch livestreams will know, when you are viewing a stream on twitch and a new person subscribes to the person streaming, their name will pop-up on-screen during the livestream a few minutes after subscribing. YouTube started offering livestreaming quite some time ago, yet no such function exists at the time of this writing. After looking around how OBS does this in combination with Twitch, I decided to create this myself with PHP and Javascript.

It was not that difficult, and all the code is available here. If you know how to setup a server able to run PHP code, and change the MailClient details to your details, this will work for you. The username is the email-adress you have used for your YouTube account.

        $imapPath = '{}INBOX';
        $username = 'username';
        $password = 'password';

I hacked it together during the weekend so there are some rough edges here and there which will be filtered out later. But, it is working and is currently used by YouTube livestreamers. Considering that all the code is available on GitHub I will not delve into it further in this blogpost. It was a quick hack and doesn't do anything that special. It reads out your email every X minutes, checks for new subscribers emails, and pushes them on-screen with a fading effect. OBS' CLR browser is running this HTML page on the background and displays whatever is displayed on the page of which the colour is not 0x0f0 (Green). (It's like using a greenscreen for special effects in movies). If you were to alter the look of the message, just remember that green text will not be visible!

The code on github is free to use for anything you like, commercial or otherwise, just give a small attribution somewhere to the original source! 😉

Crashing SimplePortal on chrome (W/ NULL terminator)

Two days ago it was discovered that it takes just a few strings to crash chrome. After playing around a bit with the malicious string, which is the URL http://a/%%30%30, I found out that by adding that URL as the image in your profile on a SimplePortal bulletin board will crash every page on which I had left a comment. Simply add the following to your profile signature.


This is enough so that the tab of anyone who clicks on a post in which you have left a comment crashes. I did report this to SimplePortal, though I am assuming they will just wait for a fix in the chromium engine.

If you want to try it out yourself without crashing your entire browser, hover over the URL here.

Pascal's Pyramid and Three.JS

As a pet-project, I wanted to render Pascal's pyramid dynamically. To do this, I have used three.js to deal with WebGL, and plain old javascript for everything non-webGL.
I have found that youtube has no video showing off a 3D visualization of Pascal's triangle so I uploaded this to youtube as well, in case you feel like checking that out!

The pyramid

Pascal's pyramid is the 3-simplex variant of pascal's Triangle. Whilst a lot of people are familiar with the 2-simplex, the 3-simplex is a bit less common. So what is the 3 simplex?
Simply put, it's the result of aligning the coefficients of trinomial expansion and the trinomial distribution in three dimensions. Technically, doing this will result in a tetrahedron and not a pyramid, since a pyramid has five faces and the generation will only create four. (1 base and three sides).

To generate a layer of the pyramid, we take the equation (A+B+C) and raise it to the power of the layer we want to generate. For example, suppose we want to generate the fourth layer, we would raise (A+B+C)^4. The trinomial is expanded by repeatedly multiplying the trinomial with itself, resulting in (A+B+C)^1 x (A+B+C)^n) == (A+B+C)^(n+1). This is the parallel of pascal's triangle in that we would use a binomial to generate a two-dimensional representation of the coeffecients of binomial expansion. (a+b)² = a² + 2ab + b² = 1a² + 2xy + 1y².

A nice thing to notice however, is that the trinomial is not the "upper-limit" of simplex'. In fact, we can generalize Pascal's Triangle into as much dimensions as we want based on the multinomial theorem. This results in us being able to generate an n-simplex, however we hit the limits of visualization at three dimensions. Mathematically however, we can go as far as we want in this.

Relation of triangle to pyramid

There is a deep relationship between Pascal's Triangle and Pascal's Pyramid. It is most easily expressed with this equation: PT(i,j) * PT(n,i) = PP(n,i,j). Where PT is Pascal's Triangle, PP is Pascal's pyramid, i is a row, j is a column and n is a level. Because of this, we don't have to code the trinomial expansion, which would be computationally speaking expensive. We can abuse the relatively easy-to-generate Pascal's Triangle and reach our Pyramid using multiplication. It states that the "nth" layer of the pyramid is generated by multiplying the numbers of each line of PT down to N by the numbers of row N.

To generate a level in Javascript I wrote

function generatePascalPyramidLevel(level)

    // First we need the row of pascal's triangle at this level.
    var trianglerow = generatePascalTriangleRow(level);

    var pyramidlevel = [];
    for(var i = 0; i <= level; i++) // i is a row in this case.
        var levelrow = [];
        var multiplicationRow = generatePascalTriangleRow(i);
        if(i == 0)
           levelrow = [1];
          for(var k = 0; k < multiplicationRow[0].length; k++)
                levelrow[k] = multiplicationRow[0][k] * trianglerow[0][i];
        pyramidlevel[i] = levelrow;
 return pyramidlevel;


It's a neat little bit of code that solves much of the computational complexity by using our triangle.

The three.js part!

Three.js is a 3D Library that makes WebGL simpler. If you're like me and you're not fond of dealing with graphics programming, it's well worth checking out due to it's ease-of-use (at least, assuming you know javascript and perhaps a bit of general 3D doesn't hurt, but there's plenty of documentation).

Displaying the triangle

First we set up our scene and our renderer. I just render it on an HTML page which has only a little bit of extra DOM elements. Namely an input text field and a 'display' button, so a user can input the amount of levels he wants to generate. After our setup, we want to get our data from the function shown above.

  var scene = new THREE.Scene();
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight-125);
    camera.position.z = 5;
    // end setup

    var input = document.getElementById("rows").value;

Now we have our scene setup, we need to render something. After some looking around the documentation and various resources I found that using ShapeGeometry to display characters rather than TextGeometry would be suitable for this project. Another thing we need to keep in mind is that the "position" of the elements is different for each element. But it can be taught of in the following way. For each layer, for each row we start at the same position. We change the position for each column we print. After a column is printed, we begin printing at our original position on the YZ axis, but change the X-axis. After the level (Row x Columns) is done, we adjust the Y position. (Go one lower to print the next level lower).

 var xoffset = 0;
    var yoffset = 2;
    var zoffset = 0;
    var colour = 0xffff0000;
    for(var level = 0; level < pascalpyramid.length; level++)
        zoffset = 0;
        for(var row = 0; row < pascalpyramid[level].length;row++)
            xoffset = 0;
            for(var col = 0; col < pascalpyramid[level][row].length; col++)

                var currentNumber = pascalpyramid[level][row][col];
                shapes = THREE.FontUtils.generateShapes(Math.round(currentNumber), {
                    font: "helvetiker",
                    weight: "normal",
                    size: 0.25

                xoffset += 1; 
                geom = new THREE.ShapeGeometry(shapes);
                mat = new THREE.MeshBasicMaterial({color:colour});
                mat.side = THREE.DoubleSide;
                mesh = new THREE.Mesh(geom, mat);
                mesh.position.set(xoffset, yoffset, zoffset);

            // Adjust the Z-axis for every column (so we display columns along Z-axis)
            zoffset -= 0.5;
yoffset -= 1;

And that's it, I added some more code to generate each layer in repeating blue-red colouring but that's trivial going from this point. I also implemented some camera functions but those are not specific to this project, though the code is included on github!

You can play around with it yourself here.


Sean McGee quickly whipped up an interested way of visualizing the layers. He did this in Photoshop but by the time you check out the code or website, it might already be implemented in my project. Though, it might not, so here's a picture!


The full code is available on github.

three.js billboard effect

If you want a certain node in three.js to always face the camera - you are looking for the billboard effect. Having a node constantly face the camera even after rotation is a problem solved with Quaternions. Quaternions are a number system that extend the complex numbers first described by the mathematician William Rowan Hamilton in 1843.

The paper is worth a read - but luckily you don't have to know all of that to create the billboard effect in three.js. In your render loop, you can update a mesh' quaternion data with the quaternion data of the camera.

    function render() {
        renderer.render(scene, camera);

        // billboard effect goes here.
        scene.traverse(function (node)
            if(node instanceof THREE.Mesh)

And that's it! You loop over every mesh in your scene and change the quaternion!

If you want to see it in action, you can generate a pyramid. and move around with the WASD (ZQSD) and arrow keys.

Quantum Entanglement explained with Parisian Zigzag

I do not usually post about physics on this blog - but I thought this particular paper published on deserves some attention.

(Abstract of paper - minor modification for blog)
Correlations related to quantum entanglement have convinced many physicists that there must be some at-a-distance connection between separated events, at the quantum level. In the late 1940’s,
however, O. Costa de Beauregard proposed that such correlations can be explained without action at a distance, so long as the influence takes a zigzag path, via the intersecting past lightcones of the events in question.Costa de Beauregard’s proposal is related to what has come to be called the retrocausal loophole in Bell’s Theorem, but – like that loophole – it receives little attention, and remains poorly understood.

Here we propose a new way to explain and motivate the idea. We exploit some simple symmetries to show how Costa de Beauregard’s zigzag needs to work, to explain the correlations at the core of Bell’s Theorem. As a bonus, the explanation shows how entanglement might be a much simpler matter than the orthodox view assumes – not a puzzling feature of quantum reality itself, but an entirely unpuzzling feature of our knowledge of reality, once zigzags are in play.

"cloud" notepad (PHP)

Whenever I am playing Steam games I like to take notes - unfortunately the only way I could access my notes made on steam was to either make documents on a service such as google docs or onedrive, or alternatively mail myself my notes. docs and onedrive have quite a bit of overhead I did not want - I just wanted something to quickly note down things such as "this link x.y.z could be interesting for project µ".

Making notepad in itself was easy enough - but a friend of mine wanted access to this as well so my original approach fell short here. Originally, I had a .txt file on the server which would be modified on a simple HTML page using a bit of jquery and javascript. Now, I decided to not create a complete hack of a project (two .txt files) - but to actually use a database, and provide a login for him and me - and possibly other friends wanting to use notepad. The project, as it stands at the time of this writing, has to following basic features.

  • Login system
  • Register*
  • CRUD notes

*register: uses a token so only people with the given token have access to this page.

As this was the second time I opted for PHP for a project (server requirement), some of the things here might be blantantly against PHP convention dictates. For instance, I follow Java standards for variable names and have implemented the Front-controller in a way similar to that of a Java webservlet. Otherwise, it should be a pretty standard OOP project.

(created using PhpStorm UML creator)

Most of this should be self-explanatory. The Servlet talks to a Facade to get access to "backend" components. The IDatabase insures a few operations the database can perform and the class "OnlineDatabase" is an implementation using MySQL. MySQLDatabase would probably have been a better name - but I do not foresee it switching to postgresql anytime soon due to, once again, server requirements. OfflineDatabase would have been the implementation with a .txt file on the servers HDD - but I did not take time to fully expand on this original 'hack' any further.

Note.php and User.php provide mapping between the database and the php code. I dubbed them "POPHPOs" in the comments - As calling them "POJOs" would have been one step too far, and part of the PHP community might already cringe at the sight of having a Servlet.

Although I believe most of this code is interchangeable between Java and PHP, or at least has a high degree of similarity, I will not put the whole code here. Just some notable things that might not be done in standard PHP that I want to highlight! Admittedly, I have no idea how the PHP community handles these things - and there might be more similarity between Java and PHP than I am aware of.

First of all, the notation of having a Servlet. Every call is made to a page called index.php, which calls the Servlet's processRequest.

require_once 'php/controller/Servlet.php';
$servlet = new Servlet();

Note that I did leave this out of the UML - as I do not consider this class to be interesting enough. (Just like a crypto class for the passwords in the facade).
In our Servlet - we check wether or not there is a post or get argument with the name "action".

A small piece of the code would be the following.

 public function processRequest()
        $this->redirect = true;
        // echo 'processing';
        $action = "login";
        if (isset($_GET['action']))
            $action = $_GET['action'];
            $action = $_POST['action'];
       $nextPage = "";

        if ($action == "gotologin")
            $nextPage = 'home.php';
            // try some database stuff
        } elseif($action == ...){..}...

I realize now that PHP might actually have support for switching on Strings (Scratch that, php HAS switching on strings). This kind of gives away that my previous projects have been constrained by old JDKs. The redirect flag is true for most cases in the pseudo-switch, but sometimes you just want to pass on data without having it redirect to another page. (Providing data with AJAX for example).

Posting the whole code here would become tedious - if you are interested in an explanation with the UML diagram and full code, feel free to mail me at (meeusdylan(at)hotmail(dot)com). Furthermore, I can not help feeling like the usage of global variables as a way to pass on data in PHP is a hack. Nevertheless - always choose the language which is best for the job, PHP was the best choice of the choices which were available to me. (Which were limited by server constraints, I frankly would have used Java I could have - because it would have been done in half the time this project took. Even though it just took about 2 days - the mistakes made due to a lack of understanding of PHP were wasted time. Though time you enjoy wasting is never really wasted time!

Design-wise it was a quick and dirty job: bootstrap and minimal jquery effects. Design is not my thing so spending time on this would be time wasted - especially with bootstrap.

Create note.



The login and register are standard bootstrap login/register pages.

It was a fun project, quite a bit for nostalgic reasons as well. I got into programming so I could make my own tools that I needed - but have been preoccupied with making things for either business or university - not for myself because I needed it. Now, I can happy scratch down some notes while playing games - or on my smartphone - and access them on my desktop. Add on that the joy of working with a language you are not good in - and completing it - and you have the best possible way to spend your time.