🖥️My proposal: a holographic computer interface
I didn't use any AI to generate the code, not even code completions.
I wasn't sure about what did we have to do in this challenge. In previous challenges, we had to make a small script, but this time it looks like we have to do a whole project. So I made a simulator of a kind of holographic computer interface. My code uses 3D CSS features to simulate 3D holographic windows.
You can see the project in the link below. You can interact with some windows, for example, you can send messages to the different chats or draw in the painting app. You can move the windows by holding them by the top, like a normal computer.
Hope you like it.
https://iamarobot123.github.io/holo/
(The link redirects to my GitHub Page, but I just use It for this kind of projects.)
Anyway, thanks to the challenge, I learned a lot I didn't know about CSS transformations. I think I have to use 3D CSS tranformations more often for my personal projects. I really enjoyed this challenge.
Here is the full code:
HTML <index.html>
<!DOCTYPE html><html> <head> <meta charset="utf-8" /> <link href="styles/main.css" rel="stylesheet" /> <title>Holographic computer</title> </head> <body> <div id="3ddisplay"> <img class="bgimagecube" src="images/cube1.jpg" id="left"> <img class="bgimagecube" src="images/cube3.jpg" id="front"> <img class="bgimagecube" src="images/cube2.jpg" id="right"> <img class="bgimagecube" src="images/cube4.jpg" id="back"> <div class="window"> <div class="main-bar"> <div class="move-window">Settings</div> <div class="close-button">X</div> </div> <div class="content holo"> <p> <span>Circle radius</span> <input type="range" class="slider" id="radius-input" min="700" max="1300" value="800"> </p> <p> <span>Opacity</span> <input type="range" class="slider" id="opacity-input" min="0.3" max="0.95" value="0.8" step="0.025"> </p> <p> <span><b style="color: #ff0060">Warning:</b> A higher oppacity may affect the energy spent by the holographic projector.</span> </p> <p> <span>Hold the wheel button or click the movement buttons to move to the sides</span> </p> </div> </div> <div class="window"> <div class="main-bar"> <div class="move-window">Messages</div> <div class="close-button">X</div> </div> <div class="content messages"> <div class="chats"> <div class="chat"> <div class="chat-profile-photo" style="background-color: #ff3030"> </div> <div class="chat-name"> Chat 1 </div> </div> <div class="chat"> <div class="chat-profile-photo" style="background-color: #30ff30"> </div> <div class="chat-name"> Chat 2 </div> </div> <div class="chat"> <div class="chat-profile-photo" style="background-color: #00bfff"> </div> <div class="chat-name"> Chat 3 </div> </div> </div> <div id="chat-display"> <div id="chat-output"> </div> <div id="chat-bar"> <input type="text" id="chat-input"> <button id="chat-send">Send</button> </div> </div> </div> </div> <div class="window"> <div class="main-bar"> <div class="move-window">Notes</div> <div class="close-button">X</div> </div> <div class="content notes"> <textarea id="notes-display">Taking notes here</textarea> <div class="notes-button-container"> <button id="notes-save">Save</button> <button id="notes-reset">Reset</button> <input id="notes-filename" type="text" value="Notes" placeholder="filename"><span>.txt</span> </div> </div> </div> <div class="window"> <div class="main-bar"> <div class="move-window">Paint</div> <div class="close-button">X</div> </div> <div class="content paint"> <canvas id="paint-display">Something went wrong</canvas> <div class="paint-button-container"> <button id="paint-save">Save</button> <button id="paint-reset">Reset</button> <input id="paint-filename" type="text" value="Painting" placeholder="filename"><span>.png</span> </div> </div> </div> </div> <div class="move-button left"> ← </div> <div class="move-button right"> → </div> <script src="scripts/main.js"></script> </body></html>
CSS <styles/main.css>
.bgimagecube { background-color: #908070; width: 1000px; height: 1000px; margin: 0; padding: 0; position: absolute; grid-template-rows: 20px calc(100% - 20px);}.window { background-color: #a0c0ff; background-image: repeating-linear-gradient(#90b0ff 5px, #c0c0ff 10px); opacity: 0.8; border-radius: 8px; position: absolute; resize: both; overflow: auto; display: grid; width: 700px; height: 500px; margin: 0; padding: 0; grid-template-rows: 20px calc(100% - 20px);}body { margin: 0px; height: 100svh; width: 100svw; overflow: hidden;}.main-bar { display: grid; background-image: repeating-linear-gradient(#6090ff 5px, #80a0ff 10px); grid-template-columns: calc(100% - 20px) 20px; margin: 0; padding: 0; cursor: grab;}.move-window { color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif; padding: 3px 7px; height: 14px;}.close-button { background-color: #ff0030; opacity: 0.8; color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif; font-size: 11pt; display: grid; padding: 3px 5px; height: 14px;}.holo { padding: 8px;}p { margin-top: 0;}.slider { margin: 0; display: inline; text-align: center; position: relative; top: 4px; cursor: pointer;}span { font-family: "Fira Sans", Arial, Helvetica, sans-serif; color: #ffffff; font-size: 11pt;}.messages { display: grid; grid-template-columns: 150px calc(100% - 150px)}.chats { display: grid; background-image: repeating-linear-gradient(#6090ff 5px, #80a0ff 10px); grid-template-rows: 60px 60px 60px;}.chat { display: grid; grid-template-columns: 50px 100px; background-color: #8090ff; border-width: 0; border-bottom: 1px; border-style: solid; border-color: #a0c0ff; cursor: pointer;}.chat-profile-photo { margin: 15px 10px; border-radius: 100%;}.chat-name { margin-top: 20px; color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif;}#chat-display { display: grid; grid-template-rows: calc(100% - 30px) 30px;}#chat-bar { display: grid; background-image: repeating-linear-gradient(#6090ff 5px, #80a0ff 10px); grid-template-columns: calc(100% - 60px) 60px;}#chat-input, #notes-filename, #paint-filename { margin: 5px; background-color: #a0c0ff; border-style: solid; border-width: 1px; border-radius: 5px; border-color: #808080;}#chat-send, #notes-save, #notes-reset, #paint-save, #paint-reset { margin: 5px; background-color: #a0c0ff; border-style: solid; border-width: 1px; border-radius: 5px; border-color: #808080; color: #404040;}#chat-send:hover, #notes-save:hover, #notes-reset:hover, #paint-save:hover, #paint-reset:hover { background-color: #c0c0ff; cursor: pointer;}.chat-header { color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif; padding: 5px; background-color: #90b0ff;}.msg1 { color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif; padding: 5px; margin: 5px; font-size: 11pt; width: fit-content; background-color: #6080ff; border-radius: 5px;}.msg2 { color: #ffffff; font-family: "Fira Sans", Arial, Helvetica, sans-serif; padding: 5px; margin: 5px; margin-left: auto; font-size: 11pt; width: fit-content; background-color: #90a0ff; border-radius: 5px;}#notes-display { width: calc(100% - 16px); height: calc(100% - 16px); border-width: 0; margin: 8px; padding: 0; display: grid; resize: none; background-color: #0000;}.notes-button-container, .paint-button-container { background-image: repeating-linear-gradient(#6090ff 5px, #80a0ff 10px);}.notes, .paint { margin: 0; padding: 0; display: grid; grid-template-rows: calc(100% - 30px) 30px;}#paint-display { width: calc(100% - 16px); height: calc(100% - 16px); border-width: 0; border-radius: 8px; margin: 8px; padding: 0; display: grid; resize: none; background-color: #e0e0ff;}.move-button { position: absolute; width: 50px; background-color: #80808080; border-radius: 8px; text-align: center; text-wrap: center; font-family: Arial, Helvetica, sans-serif; font-weight: bolder; font-size: 18px; margin-top: 5px; padding-top: 5px; padding-bottom: 7px; cursor: pointer;}.right { margin-left: calc(100% - 55px);}.left { margin-left: 5px;}
JavaScript <scripts/main.js>
const windows = [ ...document.getElementsByClassName('window')];const background = [ ...document.getElementsByClassName('bgimagecube')];const moveButtons = [ ...document.getElementsByClassName('move-button')];const radiusInput = document.getElementById('radius-input');const opacityInput = document.getElementById('opacity-input');let windowTransform = [ { direction: 0, height: 0, vh: 300, vw: 500 }, { direction: 50, height: 0, vh: 300, vw: 500 }, { direction: 100, height: 0, vh: 300, vw: 500 }, { direction: 310, height: 0, vh: 300, vw: 500 },]const deg = (n) => n / 180 * Math.PI;let c = 500;let r = 0;let o = 0;let wheelPressed = false;document.addEventListener('mousemove', (e) => { if(wheelPressed) r -= e.movementX / 5;})document.addEventListener('mousedown', (e) => { if(e.button == 1) { wheelPressed = true; e.preventDefault(); }})document.addEventListener('mouseup', (e) => { if(e.button == 1) { wheelPressed = false; e.preventDefault(); }})for(const n in moveButtons) { const assign = [-10, 10]; moveButtons[n].onclick = () => r += assign[n];}const notesSave = document.getElementById('notes-save');const notesReset = document.getElementById('notes-reset');const notesInput = document.getElementById('notes-display');const notesFileName = document.getElementById('notes-filename');notesSave.onclick = () => { const element = document.createElement('a'); element.setAttribute('href', 'data:txt;charset=utf8,' + encodeURIComponent(notesInput.value)); element.setAttribute('download', notesFileName.value); element.click();}notesReset.onclick = () => { notesInput.value = '';}const paintSave = document.getElementById('paint-save');const paintReset = document.getElementById('paint-reset');const paintInput = document.getElementById('paint-display');const ctx = paintInput.getContext('2d');const paintFileName = document.getElementById('paint-filename');paintSave.onclick = () => { const element = document.createElement('a'); element.setAttribute('href', paintInput.toDataURL('png')); element.setAttribute('download', paintFileName.value); element.click();}paintReset.onclick = () => { paintInput.height = paintInput.clientHeight; paintInput.width = paintInput.clientWidth; ctx.reset(); ctx.fillStyle = '#e0e0ff'; ctx.fillRect(0, 0, paintInput.width, paintInput.height);}paintInput.height = paintInput.clientHeight;paintInput.width = paintInput.clientWidth;ctx.fillStyle = '#e0e0ff';ctx.fillRect(0, 0, paintInput.width, paintInput.height);let pencilDown = false;let pencilX = 0, pencilY = 0;let pencilBX = 0, pencilBY = 0;paintInput.addEventListener('mousedown', (e) => { if(e.button == 0) { pencilDown = true; pencilBX = e.offsetX; pencilBY = e.offsetY; }})document.addEventListener('mouseup', (e) => { if(e.button == 0) pencilDown = false;})paintInput.addEventListener('mousemove', (e) => { pencilX = e.offsetX; pencilY = e.offsetY;})const chats = [ ...document.getElementsByClassName('chat')];const display = document.getElementById('chat-output');const input = document.getElementById('chat-input');const send = document.getElementById('chat-send');let selected = 0;const dialogues = [ ` <p class="chat-header">Chat 1</p> <p class="msg1">Hello</p> `, ` <p class="chat-header">Chat 2</p> <p class="msg1">Hi! How's it going?</p> <p class="msg2">I'm too busy right now. Let's talk later</p> <p class="msg1">Ok</p> `, ` <p class="chat-header">Chat 3</P> <p class="msg1">Do you have a moment?</p> <p class="msg1">I've to tell you something important</p> <p class="msg2">I don't have time</p> <p class="msg1">Ok, let's talk later</p> `,]for(const n in chats) { chats[n].addEventListener('click', (e) => { selected = n; })}send.onclick = (e) => { if(!input.value) return; dialogues[selected] += '<p class="msg2">' + input.value + '</p>'; input.value = '';}input.onkeydown = (e) => { if(e.key != 'Enter') return; if(!input.value) return; dialogues[selected] += '<p class="msg2">' + input.value + '</p>'; input.value = '';}const mainBars = [ ...document.getElementsByClassName('main-bar')];for(const n in mainBars) { let selected = false; mainBars[n].addEventListener('mousedown', (e) => { if(e.button == 0) selected = true; }) document.addEventListener('mouseup', (e) => { if(e.button == 0) selected = false; }) document.addEventListener('mousemove', (e) => { if(selected) { windowTransform[n].direction -= e.movementX / 7; windowTransform[n].height += e.movementY; } })}function loop(frame) { if(pencilDown) { ctx.beginPath(); ctx.moveTo(pencilBX, pencilBY); ctx.lineTo(pencilX, pencilY); ctx.lineWidth = 2; ctx.strokeStyle = '#404040'; ctx.stroke(); ctx.closePath(); pencilBX = pencilX; pencilBY = pencilY; } if(paintInput.clientHeight != paintInput.height || paintInput.clientWidth != paintInput.width) { { paintInput.height = paintInput.clientHeight; paintInput.width = paintInput.clientWidth; ctx.fillStyle = '#e0e0ff'; ctx.fillRect(0, 0, paintInput.width, paintInput.height); } } for(const n in background) { // The background is a cubemap I downloaded from the Internet. background[n].style.transform = ` translate(${window.innerWidth / 2 - 500}px, ${window.innerHeight / 2 - 500}px) perspective(1000px) translate3d(${-Math.sin(deg(r + n * 90)) * 500}px, ${0}px, ${-Math.cos(deg(r + n * 90)) * 500 + 1000}px) rotate3d(0, 1, 0, ${r + n * 90}deg) ` } c = radiusInput.value; o = opacityInput.value; for(const n in windows) { // Apply 3D CSS transformations windows[n].style.transform = '' windowTransform[n].vw = windows[n].clientWidth; windowTransform[n].vh = windows[n].clientHeight; windows[n].style.transform = ` translate(${window.innerWidth / 2 - windowTransform[n].vw / 2}px, ${window.innerHeight / 2 - windowTransform[n].vh / 2}px) perspective(1000px) translate3d(${-Math.sin(deg(windowTransform[n].direction + r)) * c}px, ${windowTransform[n].height}px, ${-Math.cos(deg(windowTransform[n].direction + r)) * c + 1000}px) rotate3d(0, 1, 0, ${windowTransform[n].direction + r}deg) ` // Apply opacity windows[n].style.opacity = `${+o + Math.random() / 30}` } display.innerHTML = dialogues[selected]; requestAnimationFrame(loop);}loop(0);
Thanks for reading. Hope you liked the idea!