Fuzzy Matching video on Webucator

April 12th, 2015

Nat Dunn of Webucator has created the following video based on my post about Sublime Text Fuzzy Matching with Javascript:

There's a lot more interesting content on his Webucator channel. I recommend checking it out!


Sublime Text like fuzzy matching in Javascript

March 5th, 2015

I recently implemented a Sublime Text like fuzzy matching for my encrypted notes app. Fuzzy matching is a really nice feature that I haven't seen used outside of code editors.

If you haven't used Sublime Text, the fuzzy matching is used to quickly open files. Rather than navigate directories in the UI – which can laborious – the open file dialogue uses the characters you type to filter a list of paths. Each character you type must match a character in the file path exactly once and and in the same order as they appear in the path. For instance the search “abgvi” would match “/application/blog/views”, as would “blgview”. The basic idea should work with any text, not just paths.

I fully expect a real Javascript programmer to do this in two lines (I'm a Python guy that has been faking Javascript proficiency for years).

My first thought in implementing this was regular expressions, but as well as matching I also wanted to highlight the matched characters in the text. That proved harder to do with a regular expression. Probably not impossible, but I'll be honest with you; I gave up.

Turns out a non-regex solution is simple enough, and plenty fast. Here it is:

function fuzzy_match(text, search)
    Parameter text is a title, search is the user's search
    // remove spaces, lower case the search so the search
    // is case insensitive
    var search = search.replace(/\ /g, '').toLowerCase();
    var tokens = [];
    var search_position = 0;

    // Go through each character in the text
    for (var n=0; n<text.length; n++)
        var text_char = text[n];
        // if we match a character in the search, highlight it
        if(search_position < search.length &&
          text_char.toLowerCase() == search[search_position])
            text_char = '<b>' + text_char + '</b>';
            search_position += 1;
    // If are characters remaining in the search text,
    // return an empty string to indicate no match
    if (search_position != search.length)
        return '';
    return tokens.join('');

This function compares a string with the fuzzy search query. If it matches, it will return the text with the matched characters wrapped in <b> tags, otherwise it returns an empty string.

I put together a demo that gets a list of links from Reddit and gives you a text box to do the fuzzy matching:


View the source if you want to know more, there are some helpful comments.


New Encrypted Notes Web Application

March 4th, 2015

The last two weekends I scratched a web development itch. I've been using KeepNote to store notes for years now. It's a nice simple desktop app, which I use to store details such as past addresses, account numbers, phone numbers etc. And more sensitive information like PIN numbers and passwords.

I configured KeepNote to store notes in Dropbox so that I don't risk losing anything. This has worked quite well, but I've always been frustrated that (a) I can't access my notes on my mobiles devices, and (b) I'm relying on a third party to keep my secrets.

My answer to this is a web application that stores notes on a server, but does the encryption in the browser (i.e. with Javascript). That way, there is no need to trust the provider. I'm not the first person to think of this (try having an original idea these days), there are some pretty good implementation of this idea. But I wanted something that is self-hosting, i.e. I could install on my own server, and I had some ideas about how the user interface should work.

In particular, I wanted to implement Sublime Text's fuzzy search. Essentially this allows you to filter the list of notes with a few key presses. For instance, if I have a note entitled ‘Social Security Number’, I can find it by typing ‘SSN’ or ‘SocSecNum’.

The site is currently live. Feel free to create a new encrypted notebook, but be aware that it is just for testing. Please don't use this to store the PIN number for your safety deposit box, or missile launch codes just yet. I will likely wipe the DB at some point.

I've create an test notebook which you can play with here:


Passphrase is: “where there is a will”

To be honest, I'm not sure what would happen if more than one person is editing a notebook at a time – but feel free to try it out.

The code is available on GitHub. I'm no cryptography expert, so I would appreciate someone who is to review the code…


“ My #Django templates in #Javascript mini-project has grown. Now supports for, if, with, filters. In only 250 loc. ”


“ Does #javascript have a version of Python's locals? Or another way of dynamically adding a local variable (name decided at runtime)? ”


“ Ken Burns effect in #Javascript and #Canvas ... http://is.gd/uBGZV1 ”


Ken Burns effect with Javascript and Canvas

February 26th, 2011

I recently decided to look into working with the Canvas element to prototype a game idea I had. Since the easiest way to learn a technology is to use it, I set myself the goal of implementing the Ken Burns Effect.

There are a few JS slideshow scripts that do the Ken Burns effect, but I haven't seen any implemented in Canvas.

Without further ado, here is my implementation of the effect:

Your browser doesn't support canvas! Try Chrome, Firefox or Opera

If you see the effect, above, you are probably viewing this in one of the good browsers. I think it can be made to run on IE with excanvas, although I have yet to test that.

If there is enough interest, I may open source the code and add some new features / docs. In the meantime, feel free to download the code (a JQuery plugin) or run it on your site:

Ken Burns Effect in Javascript and Canvas

There's actually not that much to it. Here is the code to kenburns.js:


    $.fn.kenburns = function(options) {

        var $canvas = $(this);
        var ctx = this[0].getContext('2d');
        var start_time = null;
        var width = $canvas.width();
        var height = $canvas.height();

        var image_paths = options.images;
        var display_time = options.display_time || 7000;
        var fade_time = Math.min(display_time / 2, options.fade_time || 1000);
        var solid_time = display_time - (fade_time * 2);
        var fade_ratio = fade_time - display_time
        var frames_per_second = options.frames_per_second || 30;
        var frame_time = (1 / frames_per_second) * 1000;
        var zoom_level = 1 / (options.zoom || 2);
        var clear_color = options.background_color || '#000000';

        var images = [];
        $(image_paths).each(function(i, image_path){
        function get_time() {
            var d = new Date();
            return d.getTime() - start_time;

        function interpolate_point(x1, y1, x2, y2, i) {
            // Finds a point between two other points
            return  {x: x1 + (x2 - x1) * i,
                     y: y1 + (y2 - y1) * i}

        function interpolate_rect(r1, r2, i) {
            // Blend one rect in to another
            var p1 = interpolate_point(r1[0], r1[1], r2[0], r2[1], i);
            var p2 = interpolate_point(r1[2], r1[3], r2[2], r2[3], i);
            return [p1.x, p1.y, p2.x, p2.y];

        function scale_rect(r, scale) {
            // Scale a rect around its center
            var w = r[2] - r[0];
            var h = r[3] - r[1];
            var cx = (r[2] + r[0]) / 2;
            var cy = (r[3] + r[1]) / 2;
            var scalew = w * scale;
            var scaleh = h * scale;
            return [cx - scalew/2,
                    cy - scaleh/2,
                    cx + scalew/2,
                    cy + scaleh/2];

        function fit(src_w, src_h, dst_w, dst_h) {
            // Finds the best-fit rect so that the destination can be covered
            var src_a = src_w / src_h;
            var dst_a = dst_w / dst_h;
            var w = src_h * dst_a;
            var h = src_h;
            if (w > src_w)
                var w = src_w;
                var h = src_w / dst_a;
            var x = (src_w - w) / 2;
            var y = (src_h - h) / 2;
            return [x, y, x+w, y+h];

        function get_image_info(image_index, load_callback) {
            // Gets information structure for a given index
            // Also loads the image asynchronously, if required
            var image_info = images[image_index];
            if (!image_info.initialized) {
                var image = new Image();
                image_info.image = image;
                image_info.loaded = false;
                image.onload = function(){
                    image_info.loaded = true;
                    var iw = image.width;
                    var ih = image.height;

                    var r1 = fit(iw, ih, width, height);;
                    var r2 = scale_rect(r1, zoom_level);

                    var align_x = Math.floor(Math.random() * 3) - 1;
                    var align_y = Math.floor(Math.random() * 3) - 1;
                    align_x /= 2;
                    align_y /= 2;

                    var x = r2[0];
                    r2[0] += x * align_x;
                    r2[2] += x * align_x;

                    var y = r2[1];
                    r2[1] += y * align_y;
                    r2[3] += y * align_y;

                    if (image_index % 2) {
                        image_info.r1 = r1;
                        image_info.r2 = r2;
                    else {
                        image_info.r1 = r2;
                        image_info.r2 = r1;

                    if(load_callback) {

                image_info.initialized = true;
                image.src = image_info.path;
            return image_info;

        function render_image(image_index, anim, fade) {
            // Renders a frame of the effect
            if (anim > 1) {
            var image_info = get_image_info(image_index);
            if (image_info.loaded) {
                var r = interpolate_rect(image_info.r1, image_info.r2, anim);
                var transparency = Math.min(1, fade);

                if (transparency > 0) {
                    ctx.globalAlpha = Math.min(1, transparency);
                    ctx.drawImage(image_info.image, r[0], r[1], r[2] - r[0], r[3] - r[1], 0, 0, width, height);

        function clear() {
            // Clear the canvas
            ctx.globalAlpha = 1;
            ctx.fillStyle = clear_color;
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        function update() {
            // Render the next frame
            var update_time = get_time();

            var top_frame = Math.floor(update_time / (display_time - fade_time));
            var frame_start_time = top_frame * (display_time - fade_time);
            var time_passed = update_time - frame_start_time;

            function wrap_index(i) {
                return (i + images.length) % images.length;

            if (time_passed < fade_time)
                var bottom_frame = top_frame - 1;
                var bottom_frame_start_time = frame_start_time - display_time + fade_time;
                var bottom_time_passed = update_time - bottom_frame_start_time;
                if (update_time < fade_time) {
                } else {
                    render_image(wrap_index(bottom_frame), bottom_time_passed / display_time, 1);

            render_image(wrap_index(top_frame), time_passed / display_time, time_passed / fade_time);

            if (options.post_render_callback) {
                options.post_render_callback($canvas, ctx);

            // Pre-load the next image in the sequence, so it has loaded
            // by the time we get to it
            var preload_image = wrap_index(top_frame + 1);

        // Pre-load the first two images then start a timer
        get_image_info(0, function(){
            get_image_info(1, function(){
                start_time = get_time();
                setInterval(update, frame_time);


})( jQuery );

“ When coding in #javascript I flip flop between "it is cool I can do that" and "it is NOT cool I can do that"... ”


Spatial Bookmarking Service goes Open Source

January 9th, 2011

Locidesktop was my coffee shop coding project of last year. I was quite pleased with the results. Locidesktop.com has been happily serving link desktops to some loyal visitors for months now – with no maintenance required on my part (a good thing because I've been busy with other projects).

Rather than let the project stagnate while I do other things, I have released it as open source with the hope that other Python/Django developers will pick it up and add new features. I had never really anticipated that anyone other than myself would see the code, so it is lacking in comments and documentations, but there are some nice features that may be of interest. For instance, there's a system to expose a REST API that factors out a lot of Python bioler-plate code and corresponding javascript code exposes the desktop functionality to the browser.

There's also a pretty powerful caching system that makes rendering a desktop blindingly fast. I may have gone overboard with that, given my traffic rates. But I guess it is best to have more capacity than you need.

If you want to look through the code or fork the project, it is available on Github:

Locidesktop code

The only thing missing from the open source version is a single-site theme I purchased from themeforest.net, used in a few pages such as the about & privacy urls. The desktops themselves are identical to the live site because I did those myself. You can tell because of the minimalist design reflecting my artistic abilities (minimal).

There's a README in the project that will help you get started, but other than that you may have to figure things out for yourself. It's a pretty standard Django project, although I do use Jinja templates rather than Django templates. If you have any questions, please ask them in the comments, so at least there is a central repository for issues.

For more information on Locidesktop, see my previous posts on the subject.

Update: I had to recreate the repos to remove a bunch of temp files and mercurial data. If you forked before 9am GMT, 10th Jan, you may want to get this new repos.


“ I've just realised the #Javascript is what #Python would be if it were designed by a committee and not GVR. ”

© 2008 Will McGugan.

A technoblog blog, design by Will McGugan