Showing posts with label github. Show all posts
Showing posts with label github. Show all posts

Wednesday, January 9, 2019

Web image blocks!

A specimen of the font Lobster.
Blocks of light adventure.


Find the code described in this post at GitHub!

Logical Problems

You've probably seen images laid out in such a way that they appear to be tiles or blocks in a wall where all the images are justified so there are no ragged edges. I remember that when I first did I spent a few moments casually pondering how it was down. Much, much, later I decided to create a simple website for my brother Alex (aka the pencilist) and display his art in block layout.

As the maintainer of the web site I had some requirements: I needed to be able to add new images so that they appeared as the upper left-most image, this meant that the number of rows and images that make up the rows would change; I didn't want to have to do any math when an image was added; I didn't want to copy and paste a lot of formatted HTML; I didn't want to update attribute values (like width and height) for older images when a new one was added and the older image's row might change; also what if I needed to move images around later or only display filtered images! After thinking about it for a little while I had an "A ha!" moment and came up with a solution. Now this is probably not a unique solution but it's interesting because there are two logical problems that need to be solved to create a layout like this.

Images in a row

Notice that in scrollable solutions the images are laid out in rows and each image in a row has the same height; however every row may have a different height. From this I inferred that a decision was made to include certain images in each row so the first thing I did was answer this question:

"How may images can be in a row?"

It occurred to me that a row could be defined as a vector of "blocks" each with an equal width. Based on an image's aspect ratio (width divided by height) I could assign each image a width in "blocks."

arThreeTwo: 3 / 2, // 1.5
arTwoOne: (2 / 1) + .2, // 2 (plus a small amount of buffer)

/**
 * Calculates how many blocks each image will occupy in a row.
 * @param {number} aspectRatio The width of the image divided by the height of the image.
 * @returns {number} The number of blocks.
 */
calculateBlocks: function (aspectRatio) {
    var blocks = 1; // portrait image
    if (this.arTwoOne < aspectRatio) {
        blocks = 6; // panoramic image
    } else if (this.arThreeTwo < aspectRatio) {
        blocks = 3;
    } else if (1 < aspectRatio) {
        blocks = 2.5;
    }

    return blocks;
}

I determined that I would display the images in a container so that each row was 800 pixels wide. Based on the appearance of a portrait image I decided that a row of six blocks would look good. Now iterating over the collection of all images from the beginning I create an array whose elements will represent the images in a row. I add the blocks the next image requires to the total number of blocks currently in the array, if the result is less than or equal to six I push the image onto the array otherwise a new array is created for the next row and the image is pushed onto the new array (taking care to handle the end of the collection where there may be a row that takes up less than six blocks).

I had an answer to the first question.

Proportions

The second question was much more simple to answer:

"How are the images in a row sized to fit exactly 800 pixels and all have the same height?"

The hardest part of answering this question (both questions really) is that the dimensions of some number of images has to be known; in this case at least the next six images that could potentially make up an entire row. For my implementation the information about the images is contained in an array of objects, including the dimensions of the images in the objects was trivial. However other possibilities exist including downloading a set of six images and measuring them in hidden <img> elements to determine their dimensions. This is more complex but it has greater flexibility since the size of the images doesn't need to be known ahead of time.

In any case once the image dimensions are in hand and all the images to be placed in a row are known the dimensions of the second and subsequent images in a row are scaled so that their heights match the height of the first image.

// First scale all of the image dimensions to the height of the first image.
var firstImage = images[0];
var sizes = [{ height: firstImage.height, width: firstImage.width }];

var b;
for (var i = 1; i < images.length; i++) {
    b = images[i];
    sizes.push({ height: firstImage.height, width: (firstImage.width * b.height) / b.width });
}

Add up the widths of the images to get the width of the entire row.

// Find the total width of all the scaled dimensions.
var totalScaledWidth = sizes.reduce(function (acc, x) {
    return acc + x.width;
}, 0);

Now scale the total row dimensions so the row width is 800. Doing so provides the display height that will be applied to all of the <img> elements in the row.

// Find the height that will allow all the images to fit the maximum
// width.
var newHeight = (firstImage.height * 800) / totalScaledWidth;

Finally calculate the display width of each <img> in the row.

for (var i = 0; i < images.length; i++) {
    // Update each image's dimensions to fit in the brick wall layout.
    image = images[i];
    image.displayWidth = Math.round((image.width * newHeight) / image.height);
    image.displayHeight = Math.round(newHeight);
}

Once I had the information for the images I could then set the height and width attributes of the <img> elements to size them appropriately (createElement is a helper function that returns an HTML element).

var img = createElement("img", { alt: image.title }, null, { height: image.displayHeight + "px", width: image.displayWidth + "px" });

There are a variety of concerns that are left as exercises for the reader such as: downloading the images in the desired order (newest to oldest) and so that they display reasonably quickly, indications to your user where images will appear, displaying the images in rows with proper alignment requiring a dose of CSS, and cropping images to maintain a specific row height (very long images) or minimum image width (very tall images).

Have a better solution? Let's hear about it in the comments!

Monday, June 29, 2015

Building a browser application using React

tl;dr - use Node.js-Browser-App on github to quickly get started making a React based web application built with Node.js

Lately I've been doing a lot of work constructing javascript-based applications that are meant to run in a browser. In some cases an application was meant to be used in a web browser and in other cases as a Chrome App (formerly called a Packaged App). Currently the React toolkit developed by Facebook and Instagram has achieved a certain dominance in web app development and it functions well in both environments.

One of the amazing strengths of React is the size of the ecosystem that has grown up around it. There are a large number of addons, mixins, and other projects that augment its capabilities. The sheer amount of functionality (code) available is staggering. But before I get too far into React I should really talk about Node.js. Node.js is simply a runtime environment for javascript code. Like React Node.js also has acquired a large ecosystem of code that can be executed in the runtime, perhaps most famously the "Node Package Manager" always referred to as npm.

npm helps solve one of the problems javascript projects have suffered from in the past: how to organize the code so that it can be understood by humans (file naming, directory structure, and small encapsulated chunks of functionality) and packaged for efficient delivery to a javascript runtime. One solution is to use npm packages with source code organized following CommonJS conventions. Both Node.js and npm understand CommonJS and can work with it directly but browsers can not. However as in all things Node.js + npm there's a package for that: browserify. Browserify is a toolkit that reads CommonJS files and converts them into javascript that a web browser can execute. Additionally Browserify supports "transforms" that can be applied to CommonJS source files before they are parsed and transform (modify) them in some way if necessary. Also Browserify can generate "source maps" so that later in the browser's debugger the original source files can be debugged rather than the bowserified files. As we've seen Browserify has been packaged so that it's available in the Node.js environment through npm.

Now back to React. React files are regular javascript except they support the use of JSX syntax. JSX is a little like HTML embedded in your javascript. Unfortunately neither javascript runtimes or browserify understand JSX, but there's a package for that. Through npm we have access to reactify, a browserify transform that will convert the JSX syntax into plain old javascript. Once the JSX is converted browserify can bundle up the code for a web browser.

Of course there are many other steps in preparing a web app for delivery to a server. Node.js + npm can do this too using a variety of task / build utilities, I chose to use gulp. Briefly here are some things you can do with gulp: bowserify (including reactify), conditionally affect stream piping (using gulp-if), copy and modify a JSON file (using gulp-json-editor, generate jsdoc documentation, minify output, copy files, and much more.

With Node.js + npm + gulp + browserify + reactify we can create javascript web applications where the code is organized in a manageable fashion by using CommonJS conventions and we get a high performance UI with React. If you think that those are a lot of pieces to put together from scratch you're right, so don't. You can get off to a faster start using the Node.js-Browser-App repository on github. This repository has all the components described above (plus a Flux dispatcher) so you can just copy the repo and start building a web app or Chrome App.

Incidentally, once you've got your project going you'll find a lot of cloud services can work with it. For example Microsoft's(!) Azure can be setup with webhooks from github so that every time your source code changes it will: fetch the updates from the repository, build using Node.js, and deploy the output.