2D Graphics? What’s that?

dctrDon’t forget to buy Design, Code, Test, Repeat.  It’s a fun, funny, and helpful read.

I don’t see a great need to go into too much background on 2D graphics, because the implementation in hardware frame buffers and display is not of interest to most programmers.  What is interesting when you have to do some programming is your point of view and other things that you can control easily from a software standpoint.

When you’re writing code for 2D graphics, you’re usually starting with a library or graphics environment, C#, Java, OpenGL and C++, DirectX and C++, etc.  I’m going to further limit this discussion to the C#, Java, and other environment where your drawing area starts with y=0 in the upper left hand corner with y increasing as you go down the screen.  I’ve seen too many novice programmers try to get some real graphics code written without even a hint of where to go.  I’m going to keep things somewhat esoteric for this post and start with some basic concepts that might help you if you need to develop some graphics code for a particular application.

Starting with y=0 in the upper left corner is great for text applications when you want to fill the screen and add scroll bars, but for graphs and drawings it stinks.  Wouldn’t it be more convenient to have y going up, like you learned in grade school?  What about having the origin in the middle of the screen rather than a corner, so you could have negative coordinates handled naturally?  The way to do this is NOT by hacking together a bunch of offsets and monkeying with them until the picture looks good (I learned this the hard way a long time ago).  The proper and ultimately easier way is to use simple transformations to do the job.  The main way to do this is by using a window to viewport transformation.  The idea is to take your world (the stuff you want to see), look at some or all of it (like looking out of a window at the world) and putting that onto a screen (or window on your computer), which we call the viewport.  The viewport is normally defined in normalized device coordinates (referred to as NDC, they go from 0 to 1, rather than 0 to 500, or whatever your viewport size really is).  The NDC are then scaled easily to match your actual viewport size.  There’s a simple transformation that accomplishes this and there’s a reasonable explanation here: windows to viewport.  The one thing you’ll need to add is to negate the y coordinate to make y go up instead of down.

To make your entire world visible in the viewport, you simply increase the size of your window (not the computer window, the window into your world) to the size of the stuff that’s in your world.  The transformation, then reduces this to the computer’s viewport and you see everything.  If you want to zoom in, you make the window smaller.  To see other parts of your world when you zoom in, you move the window to the right, left, up, or down.  Graphics programs usually use pan and zoom tools to accomplish these actions.

I’ll add just a couple more handy concepts to help get you started on the right foot.  Most programs are best off having a set of entities stored in memory.  I know, you’re thinking, “duh”, but I’ve seen people read files from disk on every display update and it’s not pretty.  Having a set of entities in memory allows you to quickly loop through them and display them all.  Additionally, you can calculate a bounding box for all of the entities so that you can set the window to zoom to the extents of your world.

There are some other concepts, such as clipping and selection that can come in handy, but that’s for another day and for them most part, clipping is handled automatically by the graphics engine you’re using.  Selection is also handled by some engines, too, and if not, it’s easy to implement.

If there are any concepts that you’d like more detail on, let me know by sending me an email: bill at wandercoding.com.

Comunikashion

I was reading Jeff Atwood’s post yesterday entitled, Email: The Variable Reinforcement Machine.  In it, he suggests that checking email frequently makes you less productive.  He goes on to say that various types of communication are better forums for some information than sending out emails.  As overuse of email has been a pet peeve of mine for a long time, I found it quite a good post.  However, several people misunderstood his points and are so stuck in the “email is how I live” that they closed their minds to the possibility that he had some good points to make.

His initial point was that checking your email basically interrupts your “flow”.  Flow being defined as the state in which you finally manage to boot your brain up with all relevant and necessary information needed to become productive.  This process usually takes about 15 minutes to achieve and once attained, time seems to fly by and the amount of work you accomplish can be quite large.  However, it’s easy to interrupt flow and one sure way is to check your email frequently.  Several people suggested that they don’t do that and that the answer was to turn on email notifications instead – problem solved.  But, not really.  Instead of interrupting your own flow, you now have a little popup in the corner of your screen that interrupts you automatically.

His other point, was that email is just one form of communication and not always the best for the task.  I’m not a fan of having too many venues for information, but there certainly are times where some are better than others.  If you have information that should be accessible about procedures, product/project statuses, etc., perhaps a wiki would be a good place to keep people informed about the latest and greatest.

One major value of email lies in its ability to replace the old hardcopy memo.  Used to be that you’d get a memo in your mailbox telling you about a new company policy or upcoming meeting, you’d put the memo on your desk and a few hours later, it was buried and forgotten.  Email allows for much easier distribution of mass announcements.

One area that email gets dicier is when you want an answer to a complicated question.  I think in this day and age, especially for younger folks, the art of conversation is dying.  Picking up the phone or stopping by is considered an intrusion or something to be dreaded.  It’s so much easier to send an email and simply wait for a response.  Unfortunately, this is now the default behavior, even when it’s not the best option.  I’ve slogged through so many verbose, confusing emails that I’ve had to pick up the phone and ask what was going on.  What then was the point of spending a half hour composing it in the first place.  Some things are just so difficult to describe, they deserve a conversation.

One other time that email is a time-waster is when time is of the essence.  Sending an email and waiting for a response to a time-sensitive question will send you into a tizzy while you twiddle your thumbs waiting for an answer.  Why not just call first?  If you can’t get the person, send an email as a backup to the voicemail you should have left in the first place.

I started working in software before email was used in most office environments.  Back in the day, I’d get dozens of phone calls and office visits interrupting my day.  Over the years this transformed into receiving tens or over a hundred emails a day filled with FYIs, forwarded jokes, chain letters, spam, and actually important emails.  Balancing between the two is a nearly lost art, but if you work on it, it can benefit your productivity greatly.

Run Away From This College (The Worst Salespeople)

I’ve been traveling with my family lately looking at colleges.  In April, we visited several in New England.  Campus visits usually have two main components – an information session and a tour.  They are both opportunities for colleges to show off their campuses and overall educational programs.  Unfortunately, most only do a reasonable job on one end – the tour.  After sitting through many information sessions, we finally gave up on them.  Sitting for an hour listening to an admissions officer blather about SAT scores and what made their college special became intolerable after several schools.  The best sessions used slide shows and had students help with the presentations.  The students were always more enthusiastic than the admissions people and most spoke very convincingly about why they loved the college.  The funny thing is that the professionals usually did a horrible job.  One of them began by saying, “What so special about Blah University?  You will be ripped from your comfort zone.”  Yes, he really said that.  I don’t know about you, but I don’t really look forward to that, so how do you think my high school senior daughter felt?  Next college, please.

One campus “tour” consisted mostly of walking to four places on campus (we only actually entered one building) and talking for 15 minutes at each location.  It was boring as hell and we didn’t get a very good feel for the place at all.

All in all, I still think it’s hard to pick a college and this format doesn’t work that well.  So, why don’t colleges take a hard look at the process and put their brains together to improve?  At the very least, why don’t they take a look at their existing sessions and tours and see if they can be improved?

I honestly don’t know the answer.  I don’t even think the problem is that difficult.  Some places even have feedback forms.  I suppose if you already get more college applications than spots for everyone, then there’s no incentive to improve things, which may be part of the problem.  That just leaves the issue of whether it should be illegal to bore potential customers to death.

I have been involved for software for a long time and one thing that always fascinates me is sales and marketing.  It’s not easy and I’m afraid that many companies do a poor job of it.  At one interview I had years ago, I asked what the return on investment was for their product.  They didn’t know.  I suppose they didn’t do any studies or calculations on it.  For a product that I recently developed, we knew going in that it would be easy to justify purchasing it.  It will pay for itself the first time you use it.  Who wouldn’t consider buying a product that does that?

Take a look at the marketing materials you have – paper, web sites, etc.  And take a look at the sales presentations you do?  As yourself – “If I were a customer, would I want to buy this?  Does it justify itself in terms of savings?  What about any other factors?”  If not, you’d better take a look at the product or the sales/marketing.

Morphing

I’ve been writing code for a long time – professionally, since 1986.  In that time, I’ve learned tricks and strategies for making it more robust, more flexible, faster, etc.  I’ve planned software to the nth degree and I’ve written it off the cuff.

I’m a firm believer in prototyping.  I like to get something up and running as quickly as possible so that I can grow it, play with it, see what design changes make it better, etc.  It’s very difficult to keep a lot of variables in your head at once and having thing written down, even code snippets, makes it easier to juggle thoughts.

One of my personal strategies is to keep a notebook.  I outlined this as a personal best practice in my book.  I’ve been doing this since my days as an engineering student.  Although I don’t do this as a legal document, I do find it useful for many things.  I jot down ideas and alternatives, I track to-do lists, I diagram code, and doodle icons.  Since one of my friends introduced me to engineering paper (it has a faint grid on it), I’ve preferred using quadrille ruled (like graph paper) notebooks.  This is partly because of my previous life in solid modeling and computer graphics, as graph paper makes drawing things a bit easier for me.

When I work on a new project, I try to keep in mind the big picture, but I’ve found that having a prototype makes it easy to play with ideas as long as you don’t go too deep.  I’ve tried many time to plan code solely on paper (or an electronic document), but found that, especially with an existing code-base, I’m usually wrong.  It’s irritating to find that once you’ve carefully planned your code, that as soon as you try to implement it, you find out immediately that you were planning to modify the wrong classes.  Also, the amount of planning should depend on the size of the project.

I’m also a firm believer in phased development.  Being able to give the customer (even if it’s the marketing department) something to look at and comment on, is invaluable.  People have an easier time understanding what you’re talking about when they can see it, rather than imagine it.  In my current project, I’m trying to replace an existing application that was developed as a prototype (by someone else).  Now I’m able to start over with a good idea of what my customer is looking for.  This is happening with staged development, too.  My customer currently has an application that he can play with and comment on.  Since it was developed in phases, he hasn’t had to wait for a year before there was something to work with.

On the other hand, the requirements are now growing.  This is requiring some code enhancements that I hadn’t expected and is challenging some assumptions that I’ve built in.  Fortunately, code is morphable and as long as it’s not written completely horribly, can be changed without starting over.  Constant refactoring is key to this process.  I’m continually taking code snippets and creating new methods, making all of my methods smaller.  Being happy with refactoring and not beating yourself up about prior decisions is important.  When I morph new features into my code-base, I prefer to do it piecemeal, testing along the way, rather than making giant changes and fixing dozens of bugs at the end.  In the long run, I think it produces more robust code and provides the instant gratification feedback along the way.

For example, the latest change I’m making is to allow for sub-directories in a set of C files to be compiled in my application.  I had assumed that there would be no need to have sub-directories (there weren’t any examples in the old code-base).  To accommodate this, I needed to add new menu items, new file save and load routines, and ensure that they new files would compile properly.  The easiest way to get started was to go to the end first.  I added the information to my saved files and enhanced the loading code.  I only took an hour or so to make the necessary changes and I knew that things would work.  I proceeded to work on the UI and that’s when the fun really started.  But, with refactoring and a lot more notes in my notebook, it will get done soon enough.

As Jeff Atwood points out, What’s the worst code you’ve seen recently?  My own.  Does this thrill me?  No, but I’ve learned over the years to cut myself some slack.  My code is no worse than anyone else’s, I know how to make it better, and I can morph it into a beauty.

An Itch = A Niche

In my book, Design, Code, Test, Repeat, I talk a lot about the things that make you a better and more valuable software developer (or other related software person). Many of these things are the intangibles of what makes one developer better than another. It’s hard to quantify software development. Joel Spolsky notes in his post on measurement that people usually work towards what’s being measured, making the original goal of improving overall performance using measurement, useless. This is one reason why I always hated performance reviews – you were always supposed to have measurable goals included for the year. If you were able to add a few, they were usually either bad goals that had little to do with what your job was really about or they weren’t measurable, which was more likely.

So, getting back to the intangibles. On numerous jobs, I have found myself drawn to niches. It’s not uncommon to find that one aspect of programming or your product seems to beckon. Maybe you like fussing with the UI to make it more beautiful or usable. Maybe you like the API layer because that’s what other programmers (or even your fellow programmers, plus you) will see of your product. Maybe you enjoy working more on the internals – providing core functionality, improving speed, finding new ways to solve the fundamental problems that your software is supposed to address.

These itches that pique your curiosity and passion become your niches. They become part of what make you valuable not only to your current company, but also your next potential company. When you advertise yourself on your next performance review or on your resume, be sure to include things that make you unique. They will become talking points for you in your interactions that relate to your career.

It’s important to keep your niche(s) current and flexible. Being the renowned expert in writing FORTRAN on punch cards isn’t really all that useful anymore (yeah, that’s an extreme example). Try this one – how about being an expert on a now defunct database engine? Hitching your niche to a particular technology isn’t the way to go either. It is a way to develop in-depth knowledge of an area, but it’s important to advertise the field as opposed to the specific brand. So, you’re an expert in database customization, not an expert on Debacle DataKing 5.0. The latter is something you mention later when you’re providing proof that you are a master of databases.

The final word here is to keep your niches vital. It doesn’t do any good to be the last surviving employee who remembers a particular product or technology. OK, maybe it will keep you in the company and deliver you a paycheck, but it’s unlikely to benefit you in the long-term. Keeping your niches current, finding out where your company or the industry is going, and adding them to your repertoire is what makes you valuable in the future.

Rules

We all know the adage about rules: they are made to be broken.  Rules exist to keep behavior consistent.  I don’t mind most rules when they make sense.  It would probably be frustrating if every time you called a company to ask about something, you got a different answer depending on the whims and moods of the managers or workers there.

However, rules are one thing and guidelines are another.  Recently, I’ve been calling a few companies trying to use some gift certificates.  The response has been varied.  The first company was fantastic.  I swear that everyone who works there must be taking happy-friendly pills because they have been a joy to work with.  Company #2 had more rules, but the worker bee that I spoke with decided to go the extra mile and checked with the executive management who were feeling benevolent that day.  Company #3 was a bunch of automatons.  They had similar rules to Company #2, but weren’t willing to go the extra mile.  Directly communicating with management yielded the same rule-bound thinking.  They were putting good will on the back burner.

Offering superior customer service is one thing that keeps people returning to your company.  I remember one particular CAD software company that had a superior product to everything on the market at the time.  Their salesforce, however, was arrogant and hated by their customers.  They lost the opportunity to grow because their customers didn’t want to deal with them.  In my book, Design, Code, Test, Repeat, I tell the story of another company that wanted so much money for their product that one of their customers decided to simply start their own company to compete.

In the software field, customer service, especially for smaller companies is vital.  The problem is that it can suck up vast resources before you know it.  Having your developers answer customer questions will get customers the answers that they need, but has a real drain on development productivity.  In addition, once a customer gets the direct line to a developer who was able to help, they will almost always call that developer back directly the next time they have a question.  This makes customers very happy in the short term – they get their answer.  What they don’t see is that in the long term, they may be helping that same developer work significant overtime to catch up to a schedule or slip the schedule – bad for the customer in the long term.

To help with these issues, establish fair, but flexible rules for dealing with customers.  How much does it hurt to extend a trial license for another week, for example.  Isolate your developers from direct contact, if this becomes an issue by using online forums, or by having customers call a helpline number.  Having a customer liaison, if you can afford one, will eventually make the liaison very knowledgeable (he or she will have to get the answers), but he will then be able to share the information with all customers who have the same problem (and chances are, more than one person will hit the same problem).  A liaison will also provide a more consistent experience for all of your customers – some developers should definitely not be talking to customers directly.

Keep your customers happy and you give your company a shot at repeat business and future or continued prosperity.  It only takes one bad experience to make your customers hate you and look for another place to do business.

Is It Time To Go?

Not every day is going to be a good one, but not every day should be a bad one either.  Some days it seems like you get up and follow the same routine, almost mindlessly.  You get to work, your boss is an as***le, your work is boring, blah blah blah.  A steady paycheck is good, but you can’t wait for the weekend to come.  You spend half the weekend de-stressing from the week and worrying about going back to work.  It sounds like an advertisement for an anti-depressant, doesn’t it?  All you need is a hound dog to stare at you and the picture is complete.

I’ll be the first one to tell you that the grass is not always greener.  Sometimes, it’s just a different kind of grass.  Sometimes, it’s AstroTurf.  It’s natural, however for a relationship with your company to run its course.  You’ve developed the same kind of code and done the same thing over and you need a change.

Remember, it’s easier to look for a job when you already have one.  Why?  Well, first, you’re less desperate to jump at the first opportunity.  Second, you’re clearly employable.

Life’s too short to be miserable for a long period.  Now, what’s a long period varies from person to person.  Even so, if you’ve been miserable for more than a year, it’s probably time to at least start putting your toes in the water to look for something different.  You may want to switch domains, learn new skills, switch to management or QA.  You might think that your company is looking out for you, but rest assured, unless you’re a part owner, the company is looking out for itself.  If profits are down or the stock price is down, the CEO is going to be looking to boost both, because his or her income is usually based on stock options.  When the price is down, so is the income.  If your company is looking to downsize, or already has, you can never feel safe.  Even if you’re the best coder in your group, who’s to say that the whole group isn’t going to get chopped.

It never hurts to have your resume up to date.  Keeping it fresh and current takes time, so don’t hesitate to get started today.  If you’re looking around, look for somebody who’s hiring.  Sure that sounds like a “duh” thing to say, but they may not advertise jobs in your field.  However, if they’re looking to expand in any area, then they’re thinking about the future.  Richard Bolles (author of What Color is Your Parachute) recommends that you find a company you want to work for, find the proper managers (start calling your network of friends and colleagues to get the names), and give them a quick call.  Even a friendly conversation if they’re not hiring right now can pay off later when they are hiring and remember you.

I don’t think it’s unhealthy to say “Is it time?” now and then.  If the answer is an absolute “no, I love my job”, that’s okay.  If the answer is “maybe”, well then maybe it’s time for a little exploring.  Keep your eyes open, sign up for some automated job board emails, talk to your friends and have them tell you when they run across another software engineer.  Build your network.  And good luck.

Application Programming Insanity

At a previous company, I had the pleasure of managing a couple of development groups and projects. One group consisted of four people, the other had one. My group of four was tasked with creating a printer driver, which had no API, while the group of one (is that really a group?) was writing a toolkit with an API used by many customers to create and read a very important company file format I’ll call DDT (not its real name).

DDT had been around for several years and its scope and responsibilities had grown substantially over the years. For example, while it began as a way to simply record a graphical record of one drawing, it was now being expanded to multiple drawings and meta-data about those drawings. This resulted in a significantly large amount of new code and a completely new interaction model for dealing with the new complexities.

Things didn’t start out too well in my new role. The toolkit was supposed be released in a couple of weeks. My programmer, Bob, felt that shipping a huge XML parser with his tiny toolkit wasn’t a very good idea, so he looked for an alternative. Bob was allegedly a good programmer, but was a master in his own mind. He tried his hand at writing a simple XML writer and parser, and created it in two days of solid coding. At this point, I was not reading any of his code because I didn’t understand a thing about the toolkit yet and this was not common practice at the company either. There were no performance measurements set up for the toolkit, so there was no measurement of the new XML code either. A year later, one of my other programmers investigated why the toolkit was so slow at large files and found that it was reading the XML one character at a time. Unfortunately, for 1Mb files, this is a significant chunk of time.

Meanwhile, I asked Bob if I could see the User’s Guide so I could figure out how to use the toolkit. Did I mention there was no QA on the project either? Well, there was no User’s Guide, so I decided I would write one (no documentation person either). This way I could learn to use the toolkit and give new users a shot at getting started with a clue as well. I got some sample code from Bob and began to look over the code to read a file. I asked him if he was going to create an API for users to make it easier to read the files and he sounded confused. It turns out that I was looking at the API. You know how some people who grow up driving automatic transmissions finally climb into your car with a manual transmission and say, “Wow, that’s complicated. You have to do that yourself?” This was ten times worse. Years later one of my colleagues using the same toolkit said, “It’s not really an API if you’d have an easier time just writing the code yourself.

Keeping that thought in mind, here are some basic rules for API development.

  1. If you can provide the information for the user, don’t make the user provide it to you. For example, you had to set up a time structure for the toolkit to tell you what time the file was created. This had to be done even if you didn’t care what time the file was created. Instead, provide a function or method for the user to retrieve the time.
  2. If every user needs to perform the same set of setup steps, try to condense the steps into one, especially if the user doesn’t get any information from the API during these steps. Also, see rule #1.
  3. Organize your API. If it’s a bunch of classes, use solid class designs to keep jobs related to each other. If it’s a set of functions, organize them according to jobs in your documentation – an alphabetical list may be handy for reference later, but organizing by job category will help your users learn to use the toolkit.
  4. Document it. If I’ve learned one thing over the years it’s that writing some documentation makes you think about your code. If you start explaining and get stuck describing why something has to be done before something, why it needs certain information, or can’t figure out why you’re just not doing for the user, then you have a shot at making the API simpler.
  5. Provide sample code. Like writing documentation, providing trivial and non-trivial examples are the way that most users will start their own code. They will likely start by copying your code and modifying it from there. Now, picture a thousand users doing the same thing. Are they all copying this code wondering what each line does (and understanding why they’re doing it) or is it just a blur of setup garbage that users are providing to make your toolkit happy?
  6. Idiot proof it. If you don’t think users will try passing NULL, or null, or nil, or 0 (zero) to your API because they don’t know how to get a handle to the frisbit of grommet of the window, they certainly will. Check all input data and provide useful feedback on it (or better still see rule #1 and don’t ask the user if it’s really not necessary.)

Nothing makes an API better than trying to explain it and use it. Have your colleagues or your most critical users look at it before you release it. Then, take their feedback to heart. You may save yourself dozens of support calls and hundreds of users from fleeing to someone else’s product whose API, toolkit, or product that they can understand.

Learning Vicariously

There are two (well maybe more) basic kinds of learning – what you experience yourself and what you learn through the experiences of others.  Let’s call the latter: learning vicariously.  We regularly try to do both of these kinds of learning in everyday life.  When we’re reading a how-to book, we’re learning from the experience of others.  When we try something ourselves, we’re learning first-hand.

Learning first-hand has it’s pros and cons.  If you do something wrong and learn from the experience how to do better next time or not make the same mistake again, then it did you some good.  If  you make a mistake, then repeat it, well, then the experience isn’t helping you much.  Successes and failures of this kind can be of incredible value if you take the time to evaluate the experience.  If you don’t bother to think about why something went wrong or right, then your future performance will likely be unaffected by the past, making the experience useless.  In my book, Design, Code, Test, Repeat, I tell several stories about Bob’s,  a friend, interviewing travails.  For a while, Bob went to numerous interviews and found a way to blow each one.  Finally, after some evaluation of his performance, he learned from his failures and made improvements.

Vicarious learning has similar pros and cons.  If you read about someone else’s experience, but don’t find a way to relate them to your own, then you’ve failed to learn from it.  This is why we still have thousands of drunk driving deaths every year – we fail to learn from the horrible experiences of others.  However, if you take the time to examine the stories and look at your own behavior, then your future performance can be greatly affected.

The next time you read a story, ask yourself how it relates to your own life and see what you might do better if you follow the same path, or if there is something you could avoid if you don’t repeat the same mistake.

Bleep Bleep

Although I have been unable to locate it, I remember an old Doonesbury cartoon that went something like this: Mike had a friend, Roger, visiting from MIT and was introducing him to B.D. or someone else.  When Roger spoke, all that came out was, “Bleep Bleep.”  I must admit that working around techies all day that’s what some people sound like and I’m sure that I’m no different at times.  I remember a few times when someone would come over to my desk and tell me about something they were working on in terms that were unfamiliar to me.  I would simply respond with, “Bleep Bleep.”

This came up again a couple of weeks ago at judo practice.  One of my students is a Ph.D. graduating from Cornell.  I told him that I wanted to hear his 30 second elevator description of what his work was about.  We got to talking and I mentioned how it was very important to be able to speak to people with different levels of understanding.  Not everyone in your company will have the same technical background that you do, so the ability to communicate effectively with others at their level is crucial.  You’ll often find that salesmen, marketing people, and even CEOs will use terminology incorrectly.  Additionally, they’ll often have a much less thorough background in the field than you do and yet be responsible for presenting your work to the outside world intelligently.

For example, your CEO may have to speak at a shareholders meeting.  He (or she) will need to relate the latest functionality of your software to people who want to invest in it, but have no idea how to use it.  The CEO will have to explain the technology using analogies or simpler terms that investors and analysts can understand and get excited about.  While you may be excited about the implementation, they’re only interested in the UI and general functionality.

On the other end of the spectrum, your ability to converse with your peers and other colleagues in the trenches is equally important.  You must be able to describe complex functionality in a simple way to your documentation people, who then have to help your end users understand things.  Your peers on the other hand probably will know more of your terminology, so they will be happy to use jargon that can shortcut a conversation to the point.

Knowing your audience and speaking or writing at the correct level can have a great impact on your effectiveness and your image.

« Previous PageNext Page »