In part one of our tutorial, we got a pretty good start with a grid that refreshes gracefully based on a data array. The user can toggle a single colors on and off for each pixel.
In part two we added a color palette and mouse-dragging functionality to paint pixels more quickly.
In part three we created an image tray at the bottom of the screen where users could grab different-sized canvases of their pixel art to save on their PC.
In part four we questioned whether we ought to be using React after all, and we added support for saving canvas images in localStorage.
What’s happening today?
Now we’re going to push the project to the limit: we’ll add CSS styling to make the app responsive on multiple screen sizes using media queries and flexbox.
small, narrow screen
medium screen width
normal screen width
Media queries
A media query allows us to define rules for styling elements differently depending on media type (e.g., print, screen, TV, braille) and a list of media features that include color, width, and device-aspect-ratio. Media queries allow us to define a an element once in our HTML, and then make it appear differently depending on context.
Note how we define rules for #colorDemo twice: once for the smaller screen, and once for the larger screen. In mobile-first design, we make our app look good on smaller screens first, and then we set media queries to override this behavior on a larger screen: perhaps by widening the elements, or letting them take up additional columns.
This is done since styling for smaller screens is usually simpler. Most things end up being full-width and margins and padding do not come into play so much.
We can leverage media queries in JavaScript too to change our code dynamically. It looks quite similar to the CSS. The window object has a matchMedia method that allows us to add listeners to a given CSS query of interest.
// Create the query list.
var mediaQueryList =window.matchMedia("(min-width: 500px)");
function handleOrientationChange(mql) {
isLargeScreen = mql.matches ?true:false;
if (!isLargeScreen) {
// transition to small screen
redrawAtScale(0.8);
} else {
// transition to large screen
redrawAtScale(1);
}
}
// Add the callback function as a listener to the query list.
mediaQueryList.addListener(handleOrientationChange);
// Run the orientation change handler once.
handleOrientationChange(mediaQueryList);
We’ll add an event listener to our window width and use it to dynamically change the size of the grid and the size of the preview thumbnails.
We’ll also add some CSS to make the page look nice whether the screen is small or large.
Without this setting, the width and height of elements will not be what you specify. Rather, elements will take the specified width and height as a base, adding on any padding and border that you specify (or, if you do not do a CSS reset, that a browser stylesheet has by default).
This can result in some strange behavior. Consider this box. Without border-box set, it will display as 106x106 px instead of 100x100.
Now imagine that you have a div to hold a row of boxes, and you’d like to display six per line. Well, you can’t make it 600px, because it’s going to have to hold 600 + 6*6 = 636px of width total.
You could just resize the container div. But now imagine you want to tweak the padding slightly on your elements. You’ll have to go back and resize the container div.
Row by row
We have a large container wrapper that holds all of our rows. Here’s what the container looks like on narrower screens:
We set a maximum width in pixels so that the percentage width does not become too large on large screens.
We have a line of content for each “row” on the screen.
We create a row class that is used to wrap individual rows of content. It looks the same on any type of screen size, and it scales automatically given that it’s a flexbox.
Chips can have a class activeColor when they are selected. activeColor gives a visual indication that the color is selected by spinning the chip around, raising it slightly, and giving it a box-shadow.
The saved images are those that are stored in localStorage and can be retrieved for future editing. On small screens, they’ll appear as a single row across the top of the page. We set flex-flow to row nowrap and overflow-x to cause the container to scroll when there are too many images inside to appear at once.
For screens wider than 700px, we want the saved images to appear as a vertical column to the left of the canvas. Here, we set the flex-flow to column and set a height on the div:
Canvas preview containers hold canvases saved in localStorage, or canvases that the user has generated with the “get image” button. These containers look the same everywhere. A little box-shadow makes them pop from the page, and a border on hover gives the user a hint that they are clickable and have associated actions.
On largest screens, I want to have the controls displayed above the preview images. But on smaller screens, I want to. move the controls to the very bottom and display preview images closer to the canvas.
Flexbox allows you to dynamically switch the order of elements using the order property. I number each row with an ID, row_0 through row_3.
I can then swap the position of the last two rows on smaller screens like this:
#row_3 { order: 0; }
#row_2 { order: 1; }
On larger screens, I can flip the order back to normal.
#row_3 { order: 1; }
#row_2 { order: 0; }
Nice!
By using CSS flexbox judiciously, we’re able to define a simple, row-based layout that collapses to a single column of elements on smaller screens.
Making the canvas smaller/larger
We are able to toggle the canvas size and appearance using the JavaScript media query listener we defined earlier. Remember the handleOrientationChange bit we wrote earlier?
We also wrote the method redrawAtScale. This function does the following:
let WIDTH =400;
let WIDTH_BASIS =400; // px
let HEIGHT_BASIS =400;
let HEIGHT =400; // px
WIDTH = n * WIDTH_BASIS;
HEIGHT = n * HEIGHT_BASIS;
PREVIEW_IMAGE_SCALE = n;
getCanvasAndContext();
forceRedraw();
Whenever the screen size changes, we redraw the canvas and auxiliary canvases at a fixed ratio based on the initial height and width (the HEIGHT_BASIS and WIDTH_BASIS variables). By providing a breakpoint between screen sizes, we enable the user to access the app on different-sized devices.
There you have it. TIL about responsive design, flexbox, and mobile-first CSS!