Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma

Creating A Vector-Based Content Reveal With Paper.js

By
Published in Comments (12)

A couple of days ago, I came across Paper.js - a vector graphic JavaScript library built on top of the HTML5 canvas element. Their examples are pretty awesome. I know almost nothing about the library; but, after I looked at the API for a few minutes, I wanted to see if I could use it to create a "content reveal" page overlay using a circular reveal viewport. I have no idea if this is the best way to achieve this effect (maybe SVG would be better), but after about an hour, I got something working.

The trick to this content reveal is the use of a Compound Path. A compound path is a path composed of two or more child paths. When rendered on the canvas, the parts of the child paths that overlap are "punched" out, leaving a transparent hole. To leverage this behavior, I am going to create a compound path composed of a large rectangular path and a smaller circular path. The smaller circular path should be "punched" out of the rectangular path, leaving a transparent window onto the content hidden below.

Furthermore, I am going to simulate a loading sequence (0% up to 100%) in which the inner shape - the circular viewport - will grow in size as the page is "loaded."

<!DOCTYPE html>
<html>
<head>
	<title>Creating A Page Reveal With Paper.js</title>

	<style type="text/css">

		h1 {
			font-size: 80px ;
			}

		#canvas {
			height: 100% ;
			left: 0px ;
			position: fixed ;
			top: 0px ;
			width: 100% ;
			z-index: 100 ;
			}

		#percentLabel {
			background-color: #333333 ;
			border-radius: 25px 25px 25px 25px ;
			-moz-border-radius: 25px 25px 25px 25px ;
			color: #FFFFFF ;
			font-size: 14px ;
			height: 55px ;
			left: 50% ;
			line-height: 58px ;
			margin: -23px 0px 0px -23px ;
			position: fixed ;
			text-align: center ;
			top: 50% ;
			width: 55px ;
			z-index: 200 ;
			}

	</style>

	<!-- Include the jQuery script. -->
	<script type="text/javascript" src="./jquery-1.6.1.js"></script>

	<!-- Include the Paper.js script. -->
	<script type="text/javascript" src="./paper.js"></script>
</head>
<body>

	<h1>
		Here Is A Title
	</h1>

	<p>
		Lorem ipsum dolor sit amet, consectetur adipiscing elit.
		Suspendisse hendrerit lacinia arcu sit amet adipiscing.
		Vestibulum faucibus convallis pellentesque. Aliquam
		accumsan nibh in dolor rhoncus vitae cursus sem accumsan.
		Quisque dignissim erat ac metus rutrum fermentum. Vestibulum
		volutpat, nibh in tincidunt sagittis, est felis pharetra
		lorem, eget venenatis felis magna at metus. Phasellus porta
		tempus convallis. Nunc luctus, nisl id venenatis facilisis,
		lorem arcu ultrices tortor, ac scelerisque augue massa in
		metus. Mauris sem ante, sodales vitae malesuada sed,
		malesuada sed nibh. Aenean viverra tristique dolor, vel
		vulputate massa tempor nec. In tempus nisl nec dolor lacinia
		in tempus risus ullamcorper. Phasellus lorem nisl, tincidunt
		non euismod ut, aliquet nec ligula. Phasellus laoreet
		malesuada diam, nec lobortis lectus pharetra a. Pellentesque
		mi diam, vulputate ac interdum vitae, dictum sit amet diam.
		Nunc quam mauris, feugiat in luctus eget, vulputate non nisi.
		Nullam sit amet mi nec dui feugiat aliquam. Fusce ligula quam,
		ultrices ut porta a, fringilla id quam. Pellentesque pulvinar
		ligula id odio bibendum non mollis elit commodo.
	</p>


	<!--
		This is our drawing surface. It covers the entire page
		viewport (see CSS).
	-->
	<canvas id="canvas" resize keepalive="true"></canvas>

	<!-- This is our precentage complete label. -->
	<div id="percentLabel">
		0%
	</div>


	<!--
		The Paper.js script runs inside its own script tag. I assume
		it does this so that it can create GLOBAL variables without
		actually messing up the global namespace.

		NOTE: I don't know if this is required. I didn't dive too
		deeply into the documentation. I *assume* you can use Paper.js
		without this intermediary wrapper.
	-->
	<script type="text/paperscript" canvas="canvas">


		// Get a reference to the canvas object.
		var revealCanvas = $( "#canvas" );

		// Get a reference to the perecent label.
		var percentLabel = $( "#percentLabel" );


		// I return the relevant screen/canvas coordinates needed
		// to draw the canvas object.
		function getRevealCoordinates( percentRevealed ){

			// Default the percent to zero if not provided.
			percentRevealed = (percentRevealed || 0);

			// Calculate the relevant coordinates.
			var width = revealCanvas.width();
			var height = revealCanvas.height();
			var centerX = Math.floor( width / 2 );
			var centerY = Math.floor( height / 2 );
			var complete = Math.max( centerX, centerY );
			var revealRadius = Math.floor( complete * (percentRevealed / 100) );

			// Return the coordinate collection.
			return({
				width: width,
				height: height,
				centerX: centerX,
				centerY: centerY,
				revealRadius: revealRadius
			});

		}


		// Get a reference to the current reveal path. We will
		// need this so that we can remove it between frame
		// rendering.
		var currentReveal = null;


		// I draw the current reveal over the page content.
		function drawReveal( percentRevealed ){

			// Set the fill color.
			project.currentStyle.fillColor = "black";

			// Get the releveant coordinates for drawing the reveal
			// path on the canvas.
			var coords = getRevealCoordinates( percentRevealed );

			// Create a shape for the "fill". We are going to "punch"
			// a cicrle out of this to reveal the content below.
			var screenCover = new Path.Rectangle(
				0,
				0,
				coords.width,
				coords.height
			);

			// Create our "punch" for the compound path. This
			// transparency will reveal the content behind the
			// page overlay.
			var reveal = new Path.Circle(
				new Point( coords.centerX, coords.centerY ),
				coords.revealRadius
				);

			// If there is a current overlay, remove it. Otherwise,
			// the canvas doesn't know to refresh the transparency.
			if (currentReveal){

				currentReveal.remove();

			}

			// Create the new compond path. When doing this, the
			// parts of the paths that overlap will be created as a
			// transparency (like any vector graphic program).
			currentReveal = new CompoundPath( screenCover, reveal );

			// Update the label.
			percentLabel.text( percentRevealed + "%" );

		}


		// I am the animation hook. I render the canvas.
		function onFrame(){

			// Check to see if our animation is done.
			if (percent > 100){

				// Clear our percent-simulation timer.
				clearInterval( timer );

				// Kill the onFrame() event handler.
				onFrame = null;

				// Remove the canvas and label.
				revealCanvas.remove();
				percentLabel.fadeOut();

				// Return out.
				return;

			}

			// As we near the end of our loading, we want to start
			// fading out the overlay to provide a smooth and
			// pleasing transition.
			if (percent >= 90){

				revealCanvas.css(
					"opacity",
					((100 - percent) / 10)
				);

			}

			// Draw the updated reveal overlay.
			drawReveal( percent );

		}


		// Trigger the first reveal.
		drawReveal();


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Simulate something loading.
		var percent = 0;

		var timer = setInterval(
			function(){
				percent++;
			},
			50
		);


	</script>

</body>
</html>

As you can see, the Paper.js script runs inside its own type of JavaScript tag. I couldn't find anything in the documentation to explain why this works; but, I assume it is using this approach to create global objects without actually augmenting the global namespace.

I don't really understand how this all works, especially with the onFrame() event handler; so, I won't go into any further explanation. This was just a fun experiment on my end. I did try to execute the rendering within the setInterval() callback originally; but that didn't work. I am not sure why.

I've not done too much with client-size graphical rendering. I know the canvas tag has been around for a while and SVG has been around for much longer than that. I like these little experiments; but, I can see that the client-side graphics libraries are getting super powerful. I really need to wrap my head around them and figure out how they can be used effectively within my applications.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

Nice to see these kind of tutorials coming out already.

I also found those <canvas /> attributes and custom JS MIME type quite weird. I might have a look into that.

I'm currently trying to create a quick game with Paper.js. I know its not a game framework, but it is extremely well written and super smooth, so it'd be suited to it, in many ways.

Look forward to seeing more!

15,902 Comments

@Joe,

I have to *assume* that you can do it without the custom mime-type. I see how it simplifies it (to a point); but, it also breaks all my code coloring in my IDE. Plus, I did find that there were some strange interactions with other bits of code.

That said, I agree that it looks like a really good API. What kind of game are you working on?

2 Comments

@Ben,

I was thinking of a simple Tiny Wings style platformer, taking advantage of the 'spline' style paths. That said, I don't think there's any kind of collision detection.

290 Comments

@Ben,

I'm a bit perplexed by the term vector graphics as it's being applied to the Canvas. I've written bar graph utilities using Java applets, Canvas and SVG, and my experience has been that Java AWT and Canvas are bit-mapped and only SVG is vector graphics.

The key consideration of vector graphics is whether you can resize / reposition a graphic element after it's been drawn. In bit-mapped graphics, you can't, not really: The best you can do is erase the previous element and redraw it elsewhere. If you've overlaid several graphic elements in the same space, such that some elements have been painted over top of the element you want to erase, you're screwed. You can't erase the background element without trashing whatever's been painted over top of it.

But not with vector graphics. You can overlay a bunch of SVG elements and still resize / reposition background element all you like.

You can't use jQuery to modify existing SVG elements with CSS properties, because they aren't CSS based. For example, the position of an SVG element is based on its x and y attributes, not its style.left and style.top properties. You have to use setAttribute("x", value) and setAttribute("y", value) instead. Now that attributes and properties have been separated in jQuery 1.6+, I'm not sure whether or not you can do it with .attr or .prop. I'm going to have to run some experiments, I guess.

You can also animate SVG using SMIL.

http://www.caniuse.com/#cats=SVG

15,902 Comments

@Joe,

Sounds cool.

@WebManWalking,

While the technology does, ultimately, write to a Canvas tag, I supposed the use of term "vector" applies more to the interface to said canvas. The Paper.js. library allows you to interact with the canvas as if it were a vector-based graphic package (I think). Ultimately, everything has to break to down a set of pixels at some point.

I can't say how things get "re-rendered" in any way. From the bits that I looked at, it does appear to be a heavily "object" based model.

11 Comments

This is the simple JS script I call for the canvas header to my html5 site from Scratch. It does three things, draws an arc, writes, and fills letters. You need the window.onload=function() to be able to run multiple canvases on the same page. Then you need to declare each canvas with document.getElementById("its name"). If you connect the canvas to the body tag like | | body onload="draw()" || you will only be able to have one canvas event on the page. HTML5 doesn't really use the body tag anymore. It uses header, footer, nav, section, article, and aside tags. Thus you can have var ctx2, var ctx3 and so on for each canvas run on your page. It took me awhile to figure out the window.onload=function() to be able to get multiple canvases.

window.onload=function() {

var ctx1=document.getElementById("header").getContext('2d');


ctx1.beginPath();
ctx1.strokeStyle = "rgb(200,0,0)";
ctx1.arc(100, 60,70,0,Math.PI, false);
ctx1.stroke();
ctx1.beginPath();
ctx1.textAlign="center";
ctx1.font="normal bold 30px times new roman";
ctx1.fillText("alhanson.com", 100, 30);
ctx1.font="18px times new roman";
ctx1.fillText("HTML5", 100, 75);
ctx1.font="12px times new roman";
ctx1.fillText("IPv6", 100, 110);
}

resize keepalive="true" means keep doing what you are doing tell it is done.

15,902 Comments

@Allan,

Can you expand on the "keepalive" concept? I am not sure I understand what you mean.

@Alex,

That's pretty cool! I like the clip concept. I had not heard of that before (that I can remember). I'll have to look into that.

11 Comments

@ben. Think Verbose!!!!!!!!!! The state of the canvas [with in the canvas tags] is set to a true or false. My canvas is filling letters with the [function init()] and when it is done filling the letters its state is set to is true. My canvas is drawing with the [window.onload=function()] and when it is done drawing its state is set to true. The [function init()] true means it is done. The [window.onload=function()] true means it is listening. The [window.onload] keeps the canvass on the page open and listening until the window is closed. Thus with the [window.onload] one can run many canvass on the same page. With an attribute within the canvas tag one sets its default state to true. Thus if the canvas misses a stitch, stumbles it can reset it self from with in.

Example: I use to have a satellite Internet connection. The latency on satellite is 1200 to 1800 ms up to the bird and down to the Earth and back up to the bird and down to me. One of the problems was the Window OS needed to query the DNS server to figure out what its MTU (Maximum Transmit Unit) is. By default the OS has this set to Automatic. The solution was to put a router between the computer and satellite modem and statically set the MTU in the router. Thus it was less than 1 ms for the computer to query the router and figure out what its MTU is. I think the example get more at the point I am trying to make.

1 Comments

I don't know why he's making "keepalive" so complicated.

With keepalive=true, the event loop keeps running when some other window (not your web page) is in the foreground. onFrame() code will continue to run.

With keepalive=false, the default, it does not. This is to save CPU cycles when your game or whatever is not the user's focus. onFrame() code does not run.

I did see this in the documentation somewhere, but I forget where.

11 Comments

@Cory,
This html5 OS architecture is as revolutionary change as from 3.1OS to the window OS.

The window event [window.event=function()] is as important having the computer plugged into the wall or booted up. The window is the interface that allows the software to run on the OS. The window is controlled by the registry. The registry is where the event is set to true or on. The html5 canvas tag can stream through the window dot "event" to multiple canvases in the same window. The browser and web pages are with in the client side OS window.

Another way of looking at it is like a SQL database in PhP. You open the database, query the information, and then close the database; however some people never bother to close the database. In our case the event window is open for business; however, Microsoft is dragging it feet when it comes to supporting html5. It is amazing how some bugs just disappear. Html5 is becoming popular for writing apps for smart phones; thus, Microsoft's reluctant support. They would want anyone to think they are blocking the development of other phones, over their Me phone - the phone nobody wants.

My comment# to the [ keep alive = true, the event loop ] was to Ben's comment# in his code, he didn't understand why it had to be set to true, but it did, to get the code to work. He noted, it didn't make any sense. The keep alive "while" and give focus "to" code that's running on the page is no way to be in control of the data stream from the Internet in to the window. Apparently there was a problem there that was spilling over. If one has more than one canvas on the page; when the first canvas loads, it should not affect the loading of the other canvases. The data stream to the window should not close; it should be always on - set to True. One shouldn't have to hit the window like an old TV to get it to go again. A do while not until the end loop should not run out of "do" unless it runs out of data. I believe Ben has a refresh canvas code at the end of the loop which should take care of a new reload; however, there were time I couldn't get it to fire at all. All the streamed data should eventual be in the (temp window cache) and accessible to the page and its scripts.

Note#!!!! The window temp cache is not deleted upon closing the window. It is just marked cache and isn't accessible to anything anymore that is dependable. What good is sometimes? The Window architecture comes after 3.1.

The real bug-a-boo is in the html5 offline app cache. I now have a real web site which the offline app cache is somewhat running and functional on. Now working on… working… the bugs out. The next step in the html5 progression would be to build a lap top or touch pad which would have SD card slot drives on the bottom to store off line App cache and Apps. This html5 OS architecture is as revolutionary change as from 3.1OS to the window OS.

Thus software would no longer be installed on the lap top but come as App ready to run off the SD drive. The Tablet, with chrome book type OS, like a router would store on line and on SD cards but not its hard drive. The hard drive would load the OS to Ram where it would mix it up with online and SD content.

In the future one could sent a complete 12K School and home alone University in a snail mail letter on a SD drive to seven year old in a third world remote village; which he could run on his "chrome book" OS tablet charged and powered by an old truck battery via a solar cell. His progress in school would be up dated via satellite to a server. This is an example of how my father taught his graduate students to use a combination of old and new technologies to save the earth in his vision of Earth Day.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel