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.