<!--
Made with LUME.
http://github.com/lume/lume
-->
<script src="https://assets.codepen.io/191583/LUME-5f4f19fa4d6908f1b6d74532ddb2554582e7e86e.js"></script>

<!-- Polyfill for Pointer Events (boo Safari) -->
<script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>

<!-- By default a <lume-scene> fills the space of it's parent, in this case the <body>. -->
<lume-scene experimental-webgl touch-action="none">

	<!-- Add some light to the scene! -->
	<lume-point-light size="0 0" position="0 0 1000" color="white" intensity="1.5"></lume-point-light>

	<!-- Make the grid fill 75% width and 75% height of the scene, and center it. -->
	<grid-layout size-mode="proportional proportional" size="0.75 0.75" align="0.5 0.5" mount-point="0.5 0.5">

		<!-- Each direct child of the <grid-layout> element will be positioned (its position and size
		properties will be set by the parent grid-layout). -->
		<lume-node>

			<!--
			A way to create padding is make a smaller node inside its parent.

			The <lume-dom-plane> element is a node that intends to contains traditional HTML
			content (f.e. a <img> tag), but it also renders a WebGL plane for GPU-powered shading.

			We give the WebGL plane a black coloring. We apply a material opacity in the JavaScript below.
			-->
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/car-light-streaks.jpg" />
			</lume-dom-plane>

		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/oakland-lake-merrit-sunset.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/goof-troops.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/hack-sesh.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/oreo-cookie.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/wooooo-graduation-sac-state-2014.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/ada-lovelace.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/albany-lizard.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/half-dome-yosemite.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/hawaiian-spider.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/hawaiian-starnight.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/joe-ana-anniversary-2020.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/jeremiahs-techie-selfie.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/mom-and-son.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/shelby-gt350.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/silhouettes.jpg" />
			</lume-dom-plane>
		</lume-node>

		<lume-node>
			<lume-dom-plane color="black" size-mode="proportional proportional" size="0.95 0.95">
				<img src="https://assets.codepen.io/191583/thor-beach-sunset.jpg" />
			</lume-dom-plane>
		</lume-node>

	</grid-layout>

</lume-scene>
html,
body {
	background: #222;
	width: 100%;
	height: 100%;
	margin: 0;
}

lume-scene {
	/* Prevent touch scrolling from interfering with out pointermove handler. */
	touch-action: none;
}

lume-node {
}

img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	background: black;
}
{
	// Define LUME's HTML elements with their default names.
	LUME.useDefaultNames();

	/**
	 * @mixin
	 * @class ChildWatcher -
	 * A mixin class that gives your custom element a childrenChangedCallback()
	 * that runs any time your element's children have changed while your element
	 * is connected into a live DOM tree.
	 *
	 * For example:
	 *
	 * ```js
	 * @element('my-element')
	 * class MyElement extends ChildWatcher(HTMLElement) {
	 *   childrenChangedCallback() {
	 *     console.log('The new children are:', this.children)
	 *   }
	 * }
	 * ```
	 * ```html
	 * <my-element>
	 *   <div>...</div>
	 *   <img />
	 * </my-element>
	 * ```
	 *
	 * This will log the two children of `<my-element>` when `<my-element>` is placed into the page's HTML tree.
	 */
	function ChildWatcher(Base) {
		return class ChildWatcher extends Base {
			connectedCallback() {
				super.connectedCallback?.();

				// When children have changed, recompute the layout.
				this.observer = new MutationObserver(() =>
					this.childrenChangedCallback?.()
				);
				this.observer.observe(this, { childList: true });
			}

			disconnectedCallback() {
				super.disconnectedCallback?.();
				this.observer.disconnect();
			}
		};
	}

	const { reactive, element, numberAttribute } = LUME;

	@element("grid-layout")
	class GridLayout extends ChildWatcher(LUME.Node) {
		// If rows or columns is not provided, then the default is a square grid
		// that fits all images (f.e. give 7 images, the grid rows and columns
		// would be 3 and 3, for a total of 9 cells, and two cells would be empty
		// in the last row).
		@reactive @numberAttribute(0) rows;
		@reactive @numberAttribute(0) columns;

		connectedCallback() {
			super.connectedCallback();

			// Run an initial layout on connect, and also recompute layout whenever this.rows or this.columns change.
			this.stop = LUME.autorun(() => {
				this.layout(this.rows, this.columns);
			});
		}

		disconnectedCallback() {
			// Don't forget cleanup!
			this.stop();
		}

		childrenChangedCallback() {
			// Recompute layout any time this element's children have changed
			// (it is batched, so happens once per macrotask, for better performance)
			this.layout(this.rows, this.columns);
		}

		layout(rows, columns) {
			// Calculate the grid rows and columns to be a square if rows/columns isn't provided.
			if (!rows || !columns) {
				const size = Math.ceil(Math.sqrt(this.children.length));
				rows = size;
				columns = size;
			}

			const cells = rows * columns;
			const gridSize = this.calculatedSize; // [x, y, z] in px units
			const cellWidth = gridSize.x / columns;
			const cellHeight = gridSize.y / rows;

			for (let i = 0; i < cells; i += 1) {
				const node = this.children[i];

				// If we have less nodes than total cells, quit early.
				if (!node) break;

				node.size = [cellWidth, cellHeight];
				node.position = {
					x: (i % columns) * cellWidth,
					y: Math.floor(i / columns) * cellHeight
				};
			}
		}
	}

	// The following is temporary way to set material opacity because it
	// isn't exposed through the HTML interface yet, but this shows how
	// we can manipulate the underlying Three.js objects if we so wish.
	Array.from(document.querySelectorAll("lume-dom-plane")).forEach((node) => {
		// Once the node's GL API is ready, then we can access underlying Three.js stuff.
		node.on(LUME.Events.GL_LOAD, async () => {
			// node.three in this case is a Three.js Mesh instance.
			// Three.js Mesh docs: https://threejs.org/docs/index.html#api/en/objects/Mesh
			node.three.material.opacity = 0.3;

			// If we modify properties on the underlying Three.js objects, we need to
			// manually signal to LUME that a re-render should happen:
			node.needsUpdate();
		});
	});

	Array.from(document.querySelectorAll("lume-point-light")).forEach((node) => {
		// Once the node's GL API is ready, then we can access underlying Three.js stuff.
		node.on(LUME.Events.GL_LOAD, async () => {
			// node.three in this case is a Three.js PointLight instance.
			// Three.js PointLight docs: https://threejs.org/docs/index.html#api/en/lights/PointLight
			// A little bias adjustment helps make things look smooth when the light source is further away.
			node.three.shadow.bias = -0.001;

			// If we modify properties on the underlying Three.js objects, we need to
			// manually signal to LUME that a re-render should happen:
			node.needsUpdate();
		});
	});

	const scene = document.querySelector("lume-scene");
	const light = document.querySelector("lume-point-light");
	const grid = document.querySelector("grid-layout");
	const rotationAmount = 15;

	// Add some interaction so we can see the shine from the light!
	scene.addEventListener("pointermove", (event) => {
		// Move the light on mouse move or finger drag.
		light.position.x = event.clientX;
		light.position.y = event.clientY;

		// Rotate the grid a little bit too.
		grid.rotation.y = -(
			(event.clientX / scene.calculatedSize.x) * (rotationAmount * 2) -
			rotationAmount
		);
		grid.rotation.x =
			(event.clientY / scene.calculatedSize.y) * (rotationAmount * 3) -
			rotationAmount;
	});
}
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.