html {
	overflow: hidden;
	touch-action: none;
	content-zooming: none;
}
body {
	position: absolute;
	margin: 0;
	padding: 0;
	width: 100%;
	height: 100%;
	background: #000;
}
canvas {
	position:absolute;
	width:100%;
	height:100%;
	background: #000;
}
~ function() {

	'use strict';

	var setup = {
		/* ----- setup ----- */
		zoomMin: 1.5,
		zoomMax: 6,
		spring: 0.025,
		friction: 0.95,
		aspectRatio: 1.618,
		intensity: 255,
		color: [0,0.5,1],
		ngrid: 6,
		base: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/222599/',
		content: [
			{x:2, y:0, img:'ct33.jpg'},
			{x:2, y:2, img:'ct23.jpg'},
			{x:4, y:1, img:'ci38.jpg'},
			{x:1, y:4, img:'ct11.jpg'},
			{x:3, y:4, img:'df17.jpg'},
			{x:0, y:2, img:'ct07.jpg'},
			{x:0, y:0, img:'ct14.jpg'},
			{x:5, y:5, img:'ct35.jpg'},
			{x:5, y:3, img:'ct05.jpg'},
			{x:1, y:1, w:400, h:400, canvas: function (ctx) {
				ctx.fillStyle = '#fff';
				ctx.font = 'bold 400px sans-serif';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'bottom';
				ctx.fillText('?', 200, 420);
			}},
			{x:2, y:1, w:400, h:400, canvas: function (ctx) {
				ctx.fillStyle = '#fff';
				ctx.font = 'bold 100px sans-serif';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'bottom';
				ctx.fillText('R+R-R', 200, 250);
			}}
		]
	};

	var points, planes, over, overZoomed, size, radius;
	var force    = -setup.zoomMin;
	var ngrid    =  setup.ngrid;
	var npoints  = (ngrid + 1) * (ngrid + 1);
	var nplanes  = ngrid * ngrid;

	// ---- main loop ----
	function run() {

		requestAnimationFrame(run);
		// ---- clear screen ----
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		// ---- zoom out ----
		if (over !== overZoomed) force = -setup.zoomMin;
		// ---- update points ----
		for (var i = 0; i < npoints; i++) {
			points[i].update();
		}
		// ---- update grid ----
		for (var i = 0; i < nplanes; i++) {
			planes[i].update();
		}
		// ---- zIndex sorting ----
		planes.sort(function (p0, p1) {
			return p1.zIndex - p0.zIndex;
		});
		// ---- draw grid ----
		for (var i = 0; i < nplanes; i++) {
			planes[i].draw();
		}
		// ---- tap ----
		if (pointer.isDown) {
			click();
			pointer.isDown = false;
		}
	
	}

		// ==== Points constructor ====
	var Points = function (x, y) {
		this.x  = Math.round((0.5 * (canvas.width - (size * ngrid * setup.aspectRatio))) + x * setup.aspectRatio);
		this.y  = Math.round(0.5 * (canvas.height - (size * ngrid)) + y);
		this.X  = this.x;
		this.Y  = this.y;
		this.vx = 0;
		this.vy = 0;
		this.d  = 0;
	};

	// ==== points Math ====
	Points.prototype.update = function () {
		var z, a, x, y,
			dx = pointer.x - this.x,
			dy = pointer.y - this.y;
		this.d = Math.sqrt(dx * dx + dy * dy);
		if (this.d < radius) {
			z = this.d * force * (radius - this.d) / radius;
			a = Math.atan2(dy, dx);
			x = this.x + Math.cos(a) * z;
			y = this.y + Math.sin(a) * z;
		} else {
			x = this.x;
			y = this.y;
		}
		this.vx = (this.vx + (x - this.X) * setup.spring) * setup.friction;
		this.vy = (this.vy + (y - this.Y) * setup.spring) * setup.friction;
		this.X += this.vx;
		this.Y += this.vy;
	};
	// ==== Planes constructor ====
	var Plane = function (p0, p1, p2, p3) {
		this.p0  = p0;
		this.p1  = p1;
		this.p2  = p2;
		this.p3  = p3;
		this.img = false;
		this.url = '';
		this.d2  = 0;
		this.isLoading = false;
		this.width  = 0;
		this.height = 0;
		
	};

	// ==== compute z-index ====
	Plane.prototype.update = function () {
		this.zIndex = Math.max(this.p0.d, this.p1.d, this.p2.d, this.p3.d);
	};

	// ==== draw triangle ====
	Plane.prototype.drawTriangle = function (p0, p1, p2, up) {
		if (this.isLoading) {
			if (this.img.complete) {
				this.isLoading = false;
				this.width  = this.img.width;
				this.height = this.img.height;
				this.d2     = -this.width * this.height;
			}
		} else {
			ctx.save();
			ctx.beginPath();
			ctx.moveTo(p0.X, p0.Y + up);
			ctx.lineTo(p1.X, p1.Y + up);
			ctx.lineTo(p2.X, p2.Y + up);
			ctx.closePath();
			// ---- clip ----
			ctx.clip();
			// ---- transformation matrix ----
			if (up > 0) {
				ctx.transform(
					(this.height * p0.X - this.height * p1.X) / this.d2, // m11
					(this.height * p0.Y - this.height * p1.Y) / this.d2, // m12
					(this.width  * p1.X - this.width  * p2.X) / this.d2, // m21
					(this.width  * p1.Y - this.width  * p2.Y) / this.d2, // m22
					 p0.X, // dx
					 p0.Y  // dy
				);
			} else {
				ctx.transform(
					(this.height * p2.X - this.height * p1.X) / this.d2, // m11
					(this.height * p2.Y - this.height * p1.Y) / this.d2, // m12
					(this.width  * p0.X - this.width  * p2.X) / this.d2, // m21
					(this.width  * p0.Y - this.width  * p2.Y) / this.d2, // m22
					 p0.X, // dx
					 p0.Y  // dy
				);
			}
			ctx.drawImage(this.img, 0, 0);
			ctx.restore();
		}
	};
	// ==== draw Plane ====
	Plane.prototype.draw = function () {
		// ---- path ----
		ctx.beginPath();
		ctx.moveTo(this.p0.X-1, this.p0.Y-1);
		ctx.lineTo(this.p1.X+1, this.p1.Y-1);
		ctx.lineTo(this.p2.X+1, this.p2.Y+1);
		ctx.lineTo(this.p3.X-1, this.p3.Y+1);
		ctx.closePath();
		// ---- fill color ----
		if (ctx.isPointInPath(pointer.x, pointer.y)) {
			// ---- on mouse over ----
			over = this;
			if (this.img) canvas.setCursor("pointer");
			else canvas.setCursor("default");
		}
		var c = setup.intensity - (this.zIndex / canvas.minSize * setup.intensity);
		ctx.fillStyle = 'rgb(' + 
			Math.round(c * setup.color[0]) + ',' + 
			Math.round(c * setup.color[1]) + ',' + 
			Math.round(c * setup.color[2]) + ')';
		ctx.fill();
		if (this.img) {
			// ---- draw image ----
			this.drawTriangle(this.p0, this.p1, this.p2,  1);
			this.drawTriangle(this.p0, this.p2, this.p3, -1);
		}
	};
	/* ==== build grid ==== */
	var creatGrid = function () {
		// ---- dimensions ----
		var s   = 0.8 * canvas.minSize;
		if (canvas.width < canvas.height) s = s / setup.aspectRatio;
		size    = s / ngrid;
		radius  = s / 3;
		// ---- create points ----
		points = [];
		for (var i = 0; i <= ngrid; i++) {
			for (var j = 0; j <= ngrid; j++) {
				points.push(
					new Points(
						size * j,
						size * i
					)
				);
			}
		}
		// ---- create grid ----
		planes = [];
		for (var i = 0; i < ngrid; i++) {
			for (var j = 0; j < ngrid; j++) {
				planes.push(
					new Plane(
						points[i + (ngrid + 1) * j],
						points[i + (ngrid + 1) * j + 1],
						points[i + (ngrid + 1) * (j + 1) + 1],
						points[i + (ngrid + 1) * (j + 1)]
					)
				);
			}
		}
		// ---- add grid content ----
		for (var i = 0; i < setup.content.length; i++) {
			var o = setup.content[i],
				p = planes[(o.y * 1) + ngrid * o.x];
			if (o.canvas) {
				// ---- dynamic canvas ----
				var cache = document.createElement('canvas');
				p.width = cache.width = o.w || 400;
				p.height = cache.height = o.h || 400;
				p.d2 = -p.width * p.height;
				var ict = cache.getContext('2d');
				o.canvas(ict);
				p.img = cache;
			} else {
				// ---- external image ----
				p.img = new Image();
				p.img.src = setup.base + o.img;
				p.isLoading = true;
			}
			p.url = o.url || false;
		}
	};

	var click = function () {
		if (force === -setup.zoomMin) {
			if (over.img) {
				// ---- zoom in ----
				force = -setup.zoomMax;
				overZoomed = over;
			}
		} else {
			// ---- zoom out ----
			force = -setup.zoomMin;
			// ---- link ----
			if (over.url) {
				document.location.href = over.url;
			}
		}
	};


	// set canvas

	var canvas = {  
		width:  0, 
		height: 0,
		elem: document.createElement('canvas'),
		resize: function () {
			var o = this.elem;
			var w = this.elem.offsetWidth * 1;
			var h = this.elem.offsetHeight * 1;
			if (w !== this.width || h !== this.height) {
				for (this.left = 0, this.top = 0; o !== null; o = o.offsetParent) {
					this.left += o.offsetLeft;
					this.top  += o.offsetTop;
				}
				this.width = this.elem.width = w;
				this.height = this.elem.height = h;
				this.centerX = w / 2;
				this.centerY = h / 2;
				this.minSize = Math.min(this.width, this.height);
				creatGrid();
			}
		},
		setCursor: function (type) {
			if (type !== this.cursor) {
				this.cursor = type;
				this.elem.style.cursor = type;
			}
		},
		init: function () {
			var ctx = this.elem.getContext('2d');
			document.body.appendChild(this.elem);
			window.addEventListener('resize', this.resize.bind(this), false);
			this.resize();
			return ctx;
		}
	};

	var ctx = canvas.init();

	// pointer

	var pointer = (function (canvas) {
		var pointer = {
			x: canvas.centerX,
			y: canvas.centerY,
			canvas: canvas,
			isDown: false,
			pointer: function (e) {
				var touchMode = e.targetTouches, pointer;
				if (touchMode) {
					e.preventDefault();
					pointer = touchMode[0];
				} else pointer = e;
				this.x = pointer.clientX - this.canvas.left;
				this.y = pointer.clientY - this.canvas.top;
			},
		};
		[[window, 'mousemove,touchmove', function (e) {
			this.pointer(e);
		}],
		[canvas.elem, 'mousedown,touchstart', function (e) {
			this.pointer(e); 
			this.isDown = true;
		}],
		[window, 'mouseup,touchend,touchcancel', function (e) {
			e.preventDefault();
			this.isDown = false;
		}]].forEach(function (e) {
			for (var i = 0, events = e[1].split(','); i < events.length; i++) {
				e[0].addEventListener(events[i], e[2].bind(pointer), false );
			}
		}.bind(pointer));
		return pointer;
	}(canvas));

	run();

} ();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.