Image To Cityscape – Complete Guide with HTML, CSS and JS.

Share Your Love

Here we illustrate image to cityscape with complete HTML, CSS and JS. So now see how to build?

Upload an image into Jon Kantner’s city builder and watch the city’s colors change to match!

In HTML:

<aside class="aside-open">
	<form action="">
		<button id="aside-btn" class="aside-toggle aside-toggle-open" type="button">
			<span class="sr">Toggle Panel</span>
		</button>
		<label for="img_upload">Image</label>
		<div class="upload-btn">
			<input id="img_upload" name="img_upload" type="file" accept="image/*">
			<button for="img_upload" type="button" tabindex="-1" title="Upload">
				<svg viewBox="0 0 512 512" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
					<path d="m182.461 155.48 49.539-49.539v262.059a24 24 0 0 0 48 0v-262.059l49.539 49.539a24 24 0 1 0 33.941-33.941l-90.509-90.51a24 24 0 0 0 -33.942 0l-90.509 90.51a24 24 0 1 0 33.941 33.941z"/>
					<path d="m464 232a24 24 0 0 0 -24 24v184h-368v-184a24 24 0 0 0 -48 0v192a40 40 0 0 0 40 40h384a40 40 0 0 0 40-40v-192a24 24 0 0 0 -24-24z"/>
				</svg>
			</button>
			<input id="img_name" name="img_name" type="text" placeholder="No file selected" disabled>
		</div>
		<button id="reset-btn" type="button">Reset</button>
	</form>
</aside>

In CSS:

* {
	border: 0;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
:root {
	font-size: calc(16px + (24 - 16) * (100vw - 320px) / (2560 - 320));
	--bg: #737a8c;
	--buttonBg: #2762f3;
	--buttonHoverBg: #0c48db;
	--formBg: #fff;
	--inputBorder: #abafba;
	--inputBg: #fff;
	--inputDisableBg: #e3e4e8;
	--pColor: #17181c;
}
aside {
	background: var(--formBg);
	box-shadow: 0 0 0.25em hsla(223,10%,10%,0.5);
	border-radius: 0.375em 0.375em 0 0;
	position: fixed;
	bottom: 0;
	left: 1.25em;
	max-width: 17.5em;
	width: calc(100% - 2.5em);
	transform: translateY(100%);
	transition: transform 0.3s ease-in-out;
}
body, button, input {
	color: var(--pColor);
	font: 1em/1.5 "Hind", -apple-system, sans-serif;
}
body, .upload-btn {
	overflow: hidden;
}
button:hover, button:focus, .upload-btn input[type=file]:hover + button, .upload-btn input[type=file]:focus + button {
	background: var(--buttonHoverBg);
}
button, .upload-btn input[type=file], .upload-btn input[type=file]::-webkit-file-upload-button {
	cursor: pointer;
}
button, input {
	display: block;
	width: 100%;
	-webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
}
button {
	background: var(--buttonBg);
	border-radius: 0.375em;
	color: #fff;
	margin-bottom: 1.5em;
	padding: 0.75em 1em;
	transition: background 0.1s linear;
}
button:focus {
	outline: 0;
}
form {
	padding: 1.5em 1.5em 0 1.5em;
	position: relative;
}
input {
	background: var(--inputBg);
	border-radius: 0.25em;
	box-shadow: 0 0 0 1px var(--inputBorder) inset;
	padding: 0.75em;
}
input:disabled {
	background: var(--inputDisableBg);
	cursor: not-allowed;
	text-overflow: ellipsis;
}
label {
	display: inline-block;
	font-weight: bold;
}
.aside-toggle, .aside-toggle:hover, .aside-toggle:focus {
	background-color: var(--formBg);
}
.aside-toggle {
	border-radius: 0.375em 0.375em 0 0;
	margin: 0;
	padding: 0.25em 1em;
	position: absolute;
	bottom: 100%;
	left: 50%;
	transform: translateX(-50%);
	transition: none;
	width: 4.5em;
	height: 1.5em;
}
.aside-toggle:before {
	border-left: 0.5em solid transparent;
	border-right: 0.5em solid transparent;
	border-bottom: 0.5em solid;
	color: var(--pColor);
	content: "";
	display: block;
	position: absolute;
	top: 33%;
	left: calc(50% - 0.5em);
	width: 0; 
	height: 0; 
	transition: color 0.1s linear;
}
.aside-toggle:hover:before, .aside-toggle:focus:before {
	color: var(--buttonBg);
}
.aside-open {
	transform: translateY(0%);
}
.aside-open .aside-toggle:before {
	border-bottom: 0;
	border-top: 0.5em solid;
}
.sr {
	clip: rect(1px,1px,1px,1px);
	overflow: hidden;
	position: absolute;
}
.upload-btn, .upload-btn input[type=text], .upload-btn input[type=file] + button {
	margin-bottom: 0.75em;
}
.upload-btn {
	display: flex;
	justify-content: space-between;
	position: relative;
}
.upload-btn input[type=text] {
	width: calc(62.5% - 0.375em);
}
.upload-btn input[type=file], .upload-btn input[type=file] + button {
	width: calc(37.5% - 0.375em);
}
.upload-btn input[type=file] {
	position: absolute;
	opacity: 0;
	top: 0;
	left: 0;
	height: 3em;
}
.upload-btn input[type=file] + button svg {
	display: block;
	margin: auto;
	width: 1.5em;
	height: 1.5em;
}
.upload-btn input[type=file] + button path {
	fill: #fff;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
	:root {
		--formBg: #17181c;
		--inputBg: #2e3138;
		--inputDisableBg: #2e3138;
		--inputBorder: #5c6270;
		--pColor: #e3e4e8;
	}
}

In JS:

window.addEventListener("DOMContentLoaded",app);

function app() {
	let asideBtn = document.getElementById("aside-btn"),
		resetBtn = document.getElementById("reset-btn"),
		imgUpload = document.getElementsByName("img_upload")[0],
		imgName = document.getElementsByName("img_name")[0],
		canvas = document.createElement("canvas"),
		c = canvas.getContext("2d"),
		img = null,
		scene,
		camera,
		camControls,
		renderer,
		textureLoader = new THREE.TextureLoader(),
		city,
		dust,
		// adjustable
		skyColor = 0x8fa6af,
		terrainColor = 0xe8bfa9,
		chunkSize = 64,
		gridSize = 7,
		roadWidth = 8,
		minBldgHt = 16,
		maxBldgHt = 48,
		bldgSize = 12,
		bldgFragHt = 2,
		bldgsPerChunkSide = 3,
		bldgDisplaceFactor = 0.25,
		dustParticleSpeed = 0.2,
		dustParticlesPerChunk = 12,
		sunAngle = 30,
		worldHeight = 64,
		worldSize = 1600,
		// technical
		chunkSizeHalf = chunkSize / 2,
		gridSizeEven = gridSize % 2 == 0,
		gridSizeHalf = gridSize / 2,
		bldgCellsPerSide = bldgsPerChunkSide * gridSize,
		roadsSide = chunkSize * gridSize + roadWidth,
		roadsSideHalf = roadsSide / 2,
		dustParticles = dustParticlesPerChunk * gridSize ** 2,
		// functions
		adjustWindow = () => {
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();
			renderer.setSize(window.innerWidth,window.innerHeight);
		},
		clearCity = () => {
			img = null;

			if (scene) {
				let children = city.children;

				while (children.length) {
					let child = children[0];
					// kill windows of a building
					if (child.name == "Civilian Structure") {
						let gchild = child.children[0];
						gchild.geometry.dispose();
						gchild.material.dispose();
						child.remove(gchild);
					}
					child.geometry.dispose();
					child.material.dispose();
					city.remove(child);
				}
			}
		},
		getDistance = (x1,y1,x2,y2) => {
			let raw = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2),
				rounded = Math.round(raw * 1e3) / 1e3;

			return rounded;
		},
		getLightness = (R,G,B) => {
			let r = R / 255,
				g = G / 255,
				b = B / 255,
				cmin = Math.min(r,g,b),
				cmax = Math.max(r,g,b),
				light = (cmax + cmin) / 2;

			return light;
		},
		generateCity = imgData => {
			let bldgSizeHalf = bldgSize / 2,
				bldgNegEdge = -bldgSizeHalf - 0.01,
				bldgPosEdge = bldgSizeHalf + 0.01,
				chunkDiv = chunkSize / bldgsPerChunkSide,
				chunkDivHalf = chunkDiv / 2,
				bldgTrans = -(chunkSize * gridSizeHalf) + chunkDivHalf,
				pixelIndex = 0,
				sidewalks = scene.children.filter(child => child.name == "Sidewalk");

			for (let z = 0; z < bldgCellsPerSide; ++z) {
				for (let x = 0; x < bldgCellsPerSide; ++x) {
					if (
						bldgsPerChunkSide % 2 == 0 || 
						z % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2) || 
						x % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2)
					) {
						// building
						let bldgHeight,
							bldgHeightHalf,
							bldgGeo,
							bldgMat,
							bldg,
							alpha = imgData ? imgData[pixelIndex + 3] : 0;

						if (imgData && alpha > 0.1) {
							let red = imgData[pixelIndex],
								green = imgData[pixelIndex + 1],
								blue = imgData[pixelIndex + 2],
								lightness = getLightness(red,green,blue);

							bldgHeight = minBldgHt + Math.round(lightness * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt;
							bldgHeightHalf = bldgHeight / 2;
							bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize);
							bldgMat = new THREE.MeshPhongMaterial({
								color: `rgb(${red},${green},${blue})`
							});
							bldg = new THREE.Mesh(bldgGeo,bldgMat);

						} else {
							let randHue = randomHue();
							bldgHeight = minBldgHt + Math.round(Math.random() * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt;
							bldgHeightHalf = bldgHeight / 2;
							bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize);
							bldgMat = new THREE.MeshPhongMaterial({
								color: `hsl(${randHue},50%,45%)`
							});
							bldg = new THREE.Mesh(bldgGeo,bldgMat);
						}

						bldgMat.shininess = 90;
						bldg.name = "Civilian Structure";
						bldg.castShadow = true;
						bldg.receiveShadow = true;
						bldg.position.set(
							x * chunkDiv + bldgTrans,
							bldgHeight / 2 + 1,
							z * chunkDiv + bldgTrans
						);
						// displacement towards the center of the sidewalk
						let sidewalkIndex = Math.floor(x / bldgsPerChunkSide) + (gridSize * Math.floor(z / bldgsPerChunkSide)),
							sidewalk = sidewalks[sidewalkIndex];
						if (sidewalk) {
							let sidewalkCoords = sidewalks[sidewalkIndex].position,
								sX = sidewalkCoords.x,
								bX = bldg.position.x,
								sZ = sidewalkCoords.z,
								bZ = bldg.position.z,
								distFromSWCenter = getDistance(sX,sZ,bX,bZ),
								newDist = distFromSWCenter * (1 - bldgDisplaceFactor),
								distX = Math.abs(sX - bX),
								distZ = Math.abs(sZ - bZ),
								distAngle = Math.atan(distZ / distX);

							if (bX > sX && bZ <= sZ)
								distAngle += Math.PI / 2;
							else if (bX <= sX && bZ <= sZ)
								distAngle += Math.PI;
							else if (bX <= sX && bZ > sZ)
								distAngle += Math.PI * 1.5;

							bldg.position.x = sX + (newDist * Math.sin(distAngle));
							bldg.position.z = sZ + (newDist * Math.cos(distAngle));
						}

						city.add(bldg);
						// windows
						let windowGeo = new THREE.BufferGeometry(),
							windowVertArr = [];

						for (let wy = 0; wy < bldgHeight; wy += 2) {
							for (let wx = 0; wx < 12; wx += 2) {
								let leftWinEdge = (-bldgSizeHalf + 0.5) + wx,
									rightWinEdge = (-bldgSizeHalf + 1.5) + wx,
									bottomWinEdge = (-bldgHeightHalf + 0.5) + wy,
									topWinEdge = (-bldgHeightHalf + 1.5) + wy;

								windowVertArr.push(
									// north
									rightWinEdge,bottomWinEdge,bldgNegEdge,
									leftWinEdge,bottomWinEdge,bldgNegEdge,
									leftWinEdge,topWinEdge,bldgNegEdge,
									leftWinEdge,topWinEdge,bldgNegEdge,
									rightWinEdge,topWinEdge,bldgNegEdge,
									rightWinEdge,bottomWinEdge,bldgNegEdge,
									// east
									bldgPosEdge,bottomWinEdge,rightWinEdge,
									bldgPosEdge,bottomWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,leftWinEdge,
									bldgPosEdge,topWinEdge,rightWinEdge,
									bldgPosEdge,bottomWinEdge,rightWinEdge,
									// south
									leftWinEdge,bottomWinEdge,bldgPosEdge,
									rightWinEdge,bottomWinEdge,bldgPosEdge,
									rightWinEdge,topWinEdge,bldgPosEdge,
									rightWinEdge,topWinEdge,bldgPosEdge,
									leftWinEdge,topWinEdge,bldgPosEdge,
									leftWinEdge,bottomWinEdge,bldgPosEdge,
									// west
									bldgNegEdge,bottomWinEdge,leftWinEdge,
									bldgNegEdge,bottomWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,rightWinEdge,
									bldgNegEdge,topWinEdge,leftWinEdge,
									bldgNegEdge,bottomWinEdge,leftWinEdge
								);
							}
						}

						let windowVerts = new Float32Array(windowVertArr),
							windowMat = new THREE.MeshBasicMaterial({
								color: 0x17181c
							});
							windows = new THREE.Mesh(windowGeo,windowMat);

						windowGeo.setAttribute("position",new THREE.BufferAttribute(windowVerts,3));
						bldg.add(windows);
					}
					pixelIndex += 4;
				}
			}
		},
		handleImgUpload = e => {
			return new Promise((resolve,reject) => {
				if (imgUpload) {
					let target = !e ? imgUpload : e.target;
					if (target.files.length) {
						let reader = new FileReader();
						reader.onload = e2 => {
							img = new Image();
							img.src = e2.target.result;
							img.onload = () => {
								resolve();
							};
							img.onerror = () => {
								img = null;
								reject("The image was nullified due to corruption or a non-image upload.");
							};

							if (imgName)
								imgName.placeholder = target.files[0].name;
						};
						reader.readAsDataURL(target.files[0]);
					}

				} else {
					reject("The file input is missing.");
				}
			});
		},
		imgUploadValid = () => {
			if (imgUpload) {
				let files = imgUpload.files,
					fileIsThere = files.length > 0,
					isImage = files[0].type.match("image.*"),
					valid = fileIsThere && isImage;

				return valid;

			} else {
				return false;
			}
		},
		init = () => {
			// setup
			scene = new THREE.Scene();
			scene.fog = new THREE.Fog(skyColor,512,640);
			// renderer
			renderer = new THREE.WebGLRenderer({
				logarithmicDepthBuffer: true
			});
			renderer.setClearColor(skyColor);
			renderer.setSize(window.innerWidth,window.innerHeight);
			renderer.shadowMap.enabled = true;
			// camera
			camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,0.1,1000);
			camera.position.set(160,160,160);
			camera.lookAt(scene.position);
			camControls = new THREE.OrbitControls(camera,renderer.domElement);
			camControls.enablePan = false;
			camControls.minDistance = 8;
			camControls.maxDistance = 512;
			camControls.minPolarAngle = -Math.PI / 2;
			camControls.maxPolarAngle = Math.PI / 2;
			// lighting
			let daylight = new THREE.AmbientLight(0xfbfbb6,1);
			daylight.name = "Daylight";
			scene.add(daylight);

			let sun = new THREE.PointLight(0xffffff,2,worldSize,2);
			sun.name = "Sun";
			sun.position.set(
				worldSize / 2 * Math.sin(sunAngle * Math.PI / 180),
				worldSize / 2 * Math.cos(sunAngle * Math.PI / 180),
				0
			);
			sun.castShadow = true;
			scene.add(sun);
			// terrain
			let terrainGeo = new THREE.PlaneBufferGeometry(worldSize,worldSize),
				terrainMat = new THREE.MeshStandardMaterial({
					color: terrainColor
				}),
				terrain = new THREE.Mesh(terrainGeo,terrainMat);
			terrain.name = "Terrain";
			terrain.rotation.x = -0.5 * Math.PI;
			terrain.position.y = -0.01;
			terrain.receiveShadow = true;
			scene.add(terrain);
			// roads
			let roadGeo = new THREE.PlaneBufferGeometry(roadsSide,roadsSide),
				roadMat = new THREE.MeshPhongMaterial({
					color: 0x2e3138
				}),
				road = new THREE.Mesh(roadGeo,roadMat);
			roadMat.shininess = 35;
			road.name = "Road";
			road.rotation.x = -0.5 * Math.PI;
			road.receiveShadow = true;
			scene.add(road);
			// sidewalks
			let sidewalkGeo = new THREE.BoxBufferGeometry(
					chunkSize - roadWidth,
					1,
					chunkSize - roadWidth
				),
				sidewalkMat = new THREE.MeshPhongMaterial({
					color: 0x5c6270
				}),
				sidewalk = new THREE.Mesh(sidewalkGeo,sidewalkMat);

			sidewalk.name = "Sidewalk";
			sidewalk.receiveShadow = true;

			let zStart = -Math.floor(gridSizeHalf),
				zEnd = -zStart - (gridSizeEven ? 1 : 0),
				xStart = zStart,
				xEnd = zEnd;

			for (let z = zStart; z <= zEnd; ++z) {
				for (let x = xStart; x <= xEnd; ++x) {
					let sidewalkUnit = sidewalk.clone();
					sidewalkUnit.position.set(
						chunkSize * x,
						0.5,
						chunkSize * z
					);
					if (gridSizeEven) {
						sidewalkUnit.position.x += chunkSizeHalf;
						sidewalkUnit.position.z += chunkSizeHalf;
					}
					scene.add(sidewalkUnit);
				}
			}
			// build the city
			city = new THREE.Object3D();
			city.name = "City";
			scene.add(city);
			generateCity();
			// dust particles
			let dustGeo = new THREE.BufferGeometry(),
				dustVertArr = [];

			for (let p = 0; p < dustParticles; ++p) {
				dustVertArr.push(
					Math.round(roadsSide * Math.random() - roadsSide / 2),
					Math.round(Math.random() * worldHeight),
					Math.round(roadsSide * Math.random() - roadsSide / 2)
				);
			}
			let dustVerts = new Float32Array(dustVertArr),
				dustMat = new THREE.PointsMaterial({
					map: textureLoader.load("https://i.ibb.co/mqQrvZ1/dust.png"),
					color: 0xffff00,
					size: 2,
					transparent: true
				});

			dustGeo.setAttribute("position",new THREE.BufferAttribute(dustVerts,3));
			dust = new THREE.Points(dustGeo,dustMat);
			dust.name = "Dust Particles";
			scene.add(dust);
			// render
			let body = document.body;
			body.insertBefore(renderer.domElement,body.firstChild);
			renderScene();
			// deal with preserved input
			if (imgUpload && imgUpload.value != "")
				renderPromise();
		},
		moveDust = () => {
			let posArr = dust.geometry.attributes.position.array,
				dirs = 8,
				newPosArr = posArr.map((a,i) => {
					let dim = i % 3,
						dir = i % dirs,
						angle = 360 * (dir / dirs) * Math.PI / 180;

					if (dim == 0)
						a += dustParticleSpeed * Math.sin(angle);
					else if (dim == 2)
						a += dustParticleSpeed * Math.cos(angle);

					if (dim == 0 || dim == 2) {
						a += dustParticleSpeed;
						if (a > roadsSideHalf)
							a -= roadsSide;
						else if (a < -roadsSideHalf)
							a += roadsSide;

					} else if (dim == 1) {
						a += dustParticleSpeed;
						if (a >= worldHeight)
							a = 0;
					}

					return a;
				});

			let newDustVerts = new Float32Array(newPosArr);
			dust.geometry.setAttribute("position",new THREE.BufferAttribute(newDustVerts,3));
			dust.geometry.verticesNeedUpdate = true;
		},
		randomHue = () => {
			let roundHueTo = 30,
				r = Math.floor(Math.random() * 360 / roundHueTo) * roundHueTo;
			return r;
		},
		renderPromise = e => {
			handleImgUpload(e).then(() => {
				if (imgUploadValid()) {
					updateCanvas();
					updateImg();
				}
					
			}).catch(msg => {
				console.log(msg);
			});
		},
		renderScene = () => {
			moveDust();
			renderer.render(scene,camera);
			requestAnimationFrame(renderScene);
		},
		resetCity = () => {
			if (imgName)
				imgName.placeholder = "No file selected";

			clearCity();
			generateCity();
		},
		toggleAside = e => {
			let aside = document.querySelector("aside");
			if (aside) {
				let openClass = "aside-open";
				if (e.keyCode == 27)
					aside.classList.remove(openClass);
				else if (!e.keyCode)
					aside.classList.toggle(openClass);
			}
		},
		updateCanvas = () => {
			// restrict image size, keep it proportional
			let imgWidth = img.width,
				imgHeight = img.height;

			if (imgWidth >= imgHeight) {
				if (imgWidth >= bldgCellsPerSide) {
					imgWidth = bldgCellsPerSide;
					imgHeight = imgWidth * (img.height / img.width);
				}
			} else {
				if (imgHeight >= bldgCellsPerSide) {
					imgHeight = bldgCellsPerSide;
					imgWidth = imgHeight * (img.width / img.height);
				}
			}
			// update canvas
			c.clearRect(0,0,bldgCellsPerSide,bldgCellsPerSide);

			let imgX = bldgCellsPerSide / 2 - imgWidth / 2,
				imgY = bldgCellsPerSide / 2 - imgHeight / 2;

			c.drawImage(img,imgX,imgY,imgWidth,imgHeight);
		},
		updateImg = () => {
			let imgData = c.getImageData(0,0,bldgCellsPerSide,bldgCellsPerSide),
				data = imgData.data;
			
			clearCity();
			generateCity(data);
		};

	init();

	if (asideBtn) {
		asideBtn.addEventListener("click",toggleAside);
		window.addEventListener("keydown",toggleAside);
	}
	if (resetBtn)
		resetBtn.addEventListener("click",resetCity);
	if (imgUpload)
		imgUpload.addEventListener("change",renderPromise);

	window.addEventListener("resize",adjustWindow);
}

In Codepen:

See the Pen Image to Cityscape by Jon Kantner (@jkantner) on CodePen.

Please comment and share this post and add some valuable information please WhatsApp us.

Share Your Love
Avatar photo
Lingaraj Senapati

Hey There! I am Lingaraj Senapati, the Founder of lingarajtechhub.com My skills are Freelance, Web Developer & Designer, Corporate Trainer, Digital Marketer & Youtuber.

Articles: 429

Newsletter Updates

Enter your email address below to subscribe to our newsletter