<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>XOY calibration tool powered by DroidCamX</title> <style type="text/css"> body { background:#222222; color: darkgray;} .btn { height: 3em; min-width: 3em; border-radius:5px; color: darkgray; background-color: #222222; border: 1px solid darkgray; padding: 0.2rem 0.5rem ; margin: 0.5rem; } .btn:hover { border-color: white; } #feedimg { overflow:hidden;float:left;position:relative } #feedimg img { transform-origin: top left; /* IE 10+, Firefox, etc. */ -webkit-transform-origin: top left; /* Chrome */ -ms-transform-origin: top left; /* IE 9 */ position: relative; z-index: -1; top: 2px; left: 2px; } .invisible { visibility: hidden; } #x-container { display: flex; justify-content: center; align-items: center; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 2px solid red; } th, td { padding: 0.2rem; width: 25%; text-align: center;} table input[type=number] { text-align: right; font-size: large; } </style> </head> <body> <div id="buttons" style="display: flex; justify-content: center; flex-direction: row;"> <button type="button" class="btn" onclick="feed.setDroid()" title="Set DroidCamX address"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M5 4q0-.825.588-1.413T7 2h10q.825 0 1.413.588T19 4v12q0 .425-.288.713T18 17h-1V4H7v12.925L6.925 17H6q-.425 0-.713-.288T5 16V4Zm7 6q.825 0 1.413-.588T14 8q0-.825-.588-1.413T12 6q-.825 0-1.413.588T10 8q0 .825.588 1.413T12 10ZM9.55 20H6q-.425 0-.713-.288T5 19q0-.425.288-.713T6 18h3.55l-.4-.4q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.1 2.1q.3.3.3.7t-.3.7l-2.1 2.1q-.275.275-.7.275t-.7-.275q-.275-.275-.275-.7t.275-.7l.4-.4ZM15 20q-.425 0-.713-.288T14 19q0-.425.288-.713T15 18h3q.425 0 .713.288T19 19q0 .425-.288.713T18 20h-3ZM12 8Z"/></svg> </button> <button type="button" class="btn" onclick="feed.reload()" title="Reload"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 13.5A7.5 7.5 0 1 1 11.5 6H20m0 0l-3-3m3 3l-3 3"/></svg> </button> <button type="button" id="get-droid" onclick="window.open('https://www.dev47apps.com/', '_blank')" class="btn">Get <b>DroidCam</b> <svg version="1.2" width="24" height="24" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle;"> <path d="m47.49 39.57a6.19 6.19 0 0 1-6.19 6.19h-34.1a6.19 6.19 0 0 1-6.19-6.19v-31.62a6.19 6.19 0 0 1 6.19-6.19h34.1a6.19 6.19 0 0 1 6.19 6.19v31.62z" fill="#74aa03"/> <path d="m19.84 10.28q4.37-1.48 8.77 0a0.62 0.61-62.5 0 0 0.7-0.23l2.05-2.92a0.33 0.32-14.1 0 1 0.56 0.33l-1.42 2.73a0.84 0.83-62 0 0 0.34 1.12q4.03 2.19 4.93 6.73a0.81 0.8-5.6 0 1-0.79 0.96h-21.75q-0.81 0-0.66-0.79 0.84-4.46 4.72-6.76a1.16 1.15 59.7 0 0 0.41-1.57l-1.23-2.12a0.46 0.46 0 0 1 0.78-0.48l1.84 2.75q0.28 0.41 0.75 0.25z" fill="#fff"/> <path d="m20.43 14.5a1.23 1.23 0 0 1-1.23 1.23 1.23 1.23 0 0 1-1.23-1.23 1.23 1.23 0 0 1 1.23-1.23 1.23 1.23 0 0 1 1.23 1.23z" fill="#74aa03"/> <path d="m30.42 14.5a1.23 1.23 0 0 1-1.23 1.23 1.23 1.23 0 0 1-1.23-1.23 1.23 1.23 0 0 1 1.23-1.23 1.23 1.23 0 0 1 1.23 1.23z" fill="#74aa03"/> <g fill="#fff"> <path d="m10.741 33.874a2.39 2.39 0 0 1-2.3942 2.3858l-0.18-3e-4a2.39 2.39 0 0 1-2.3858-2.3941l0.0186-10.66a2.39 2.39 0 0 1 2.3942-2.3858l0.18 3e-4a2.39 2.39 0 0 1 2.3858 2.3941l-0.0186 10.66z"/> <path d="m30.03 25.45c-10.55-9-20.82 9.99-7.6 13.94a0.57 0.57 0 0 1-0.16 1.11h-2.3q-0.53 0-0.98 0.29-2.3 1.48-4.96 0.58a2.26 2.26 0 0 1-1.53-2.14v-17.73q0-0.75 0.75-0.75h22a0.5 0.5 0 0 1 0.5 0.5v18.24a1.83 1.83 0 0 1-1.1 1.68q-2.61 1.13-4.78-0.32-0.52-0.35-1.14-0.35h-2.26a0.55 0.55 0 0 1-0.17-1.07c5.74-1.88 9.03-9.45 3.73-13.98z"/> <path d="m42.479 34.016a2.38 2.38 0 0 1-2.3758 2.3841l-0.16 3e-4a2.38 2.38 0 0 1-2.3842-2.3758l-0.0188-10.82a2.38 2.38 0 0 1 2.3758-2.3841l0.16-3e-4a2.38 2.38 0 0 1 2.3842 2.3758l0.0188 10.82z"/> <path d="m26 32.91q1.22-2.16-0.81-3.36-0.77-0.45-0.73 0.44l0.05 0.89q0.05 0.99-0.9 0.69l-0.51-0.16q-0.68-0.22-0.97 0.44l-0.53 1.24a0.55 0.54-37.8 0 1-1.04-0.13c-1.15-7.43 8.51-6.48 7.66-1.23q-0.46 2.81-4.53 3.85a0.93 0.92 28.4 0 1-0.53-1.77q1.09-0.37 2.18-0.47a0.85 0.84-77.6 0 0 0.66-0.43z"/> </g> </svg> </button> <div id="button-bar" style="display: none; justify-content: center;flex-direction: row;align-items: start;" role="group"> <button type="button" class="btn" onclick="feed.flipX()" title="Flip X"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" d="m0 15l6-5l-6-4.9zm9-4.9l6 4.9V5l-6 5.1zm5 2.8l-3.4-2.8l3.4-3v5.8zM7 5h1v1H7V5zm0-2h1v1H7V3zm0 4h1v1H7V7zm0 2h1v1H7V9zm0 2h1v1H7v-1zm0 2h1v1H7v-1zm0 2h1v1H7v-1z"/><path fill="currentColor" d="M7.5 1c1.3 0 2.6.7 3.6 1.9L10 4h3V1l-1.2 1.2C10.6.8 9.1 0 7.5 0C5.6 0 3.9 1 2.6 2.9l.8.6C4.5 1.9 5.9 1 7.5 1z"/></svg> </button> <button type="button" class="btn" onclick="feed.flipY()" title="Flip Y"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16"><path fill="currentColor" d="m1 1l5 6l4.94-6H1zm4.94 9L1 16h10zm-2.82 5l2.83-3.44l3 3.44H3.12zM10 8h1v1h-1V8zm2 0h1v1h-1V8zM8 8h1v1H8V8zM6 8h1v1H6V8zM4 8h1v1H4V8zM2 8h1v1H2V8zM0 8h1v1H0V8z"/><path fill="currentColor" d="M15 8.47a4.807 4.807 0 0 1-1.879 3.632L12 11v3h3l-1.18-1.18A5.757 5.757 0 0 0 16 8.478V8.47a6.062 6.062 0 0 0-2.884-4.905l-.596.805a5.095 5.095 0 0 1 2.479 4.087z"/></svg> </button> <button type="button" class="btn" onclick="feed.zoomIn()" title="Zoom In"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13v4m-2-2h4m-7 0a5 5 0 1 0 10 0a5 5 0 1 0-10 0m12 7l-3-3M6 18H5a2 2 0 0 1-2-2v-1m0-4v-1m0-4V5a2 2 0 0 1 2-2h1m4 0h1m4 0h1a2 2 0 0 1 2 2v1"/></svg> </button> <button type="button" class="btn" onclick="feed.zoomOut()" title="Zoom Out"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 15h4m-7 0a5 5 0 1 0 10 0a5 5 0 1 0-10 0m12 7l-3-3M6 18H5a2 2 0 0 1-2-2v-1m0-4v-1m0-4V5a2 2 0 0 1 2-2h1m4 0h1m4 0h1a2 2 0 0 1 2 2v1"/></svg> </button> <button type="button" class="btn" onclick="feed.toggleLed()" title="Toggle LED"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7"/></svg> </button> <button type="button" class="btn" onclick="feed.setAutoFocus()" title="Auto Focus"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M3 7V5a2 2 0 0 1 2-2h2m10 0h2a2 2 0 0 1 2 2v2m0 10v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/></g></svg> </button> <button type="button" class="btn" onclick="feed.lockExposure()" title="Lock Exposure"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M11 6H7a1 1 0 0 0 0 2h4a1 1 0 0 0 0-2Zm8-4H5a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3ZM4 18.59V5a1 1 0 0 1 1-1h13.59ZM20 19a1 1 0 0 1-1 1H5.41L20 5.41Zm-7-2h1v1a1 1 0 0 0 2 0v-1h1a1 1 0 0 0 0-2h-1v-1a1 1 0 0 0-2 0v1h-1a1 1 0 0 0 0 2Z"/></svg> </button> <button type="button" class="btn" onclick="feed.unlockExposure()" title="Unlock Exposure"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.6 19.4L12 12m2-2l5.4-5.4M8 4h10a2 2 0 0 1 2 2v10m-.586 3.414A2 2 0 0 1 18 20H6a2 2 0 0 1-2-2V6c0-.547.22-1.043.576-1.405"/><path d="M7 9h2v2m4 5h3M3 3l18 18"/></g></svg> </button> <button type="button" class="btn" onclick="feed.resize(-1)" title="Smaller"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20"><path fill="currentColor" d="M4.1 14.1L1 17l2 2l2.9-3.1L8 18v-6H2l2.1 2.1zM19 3l-2-2l-2.9 3.1L12 2v6h6l-2.1-2.1L19 3z"/></svg> </button> <button type="button" class="btn" onclick="feed.resize(1)" title="Larger"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20"><path fill="currentColor" d="m6.987 10.987l-2.931 3.031L2 11.589V18h6.387l-2.43-2.081l3.03-2.932l-2-2zM11.613 2l2.43 2.081l-3.03 2.932l2 2l2.931-3.031L18 8.411V2h-6.387z"/></svg> </button> <button type="button" class="btn" style="background-color: green;" onclick="feed.setColor('green')"> </button> <button type="button" class="btn" style="background-color: blue;" onclick="feed.setColor('blue')"> </button> <button type="button" class="btn" style="background-color: red;" onclick="feed.setColor('red')"> </button> <button type="button" class="btn" style="background-color: yellow;" onclick="feed.setColor('yellow')"> </button> <button type="button" class="btn" style="background-color: white;" onclick="feed.setColor('white')"> </button> </div> </div> <div style="display: flex; justify-content: center;"> <div id="feedimg"> <div id="x-container"> <svg width="36.71mm" height="36.71mm" version="1.1" viewBox="0 0 36.71 36.71" xmlns="http://www.w3.org/2000/svg"> <g id="cross" transform="translate(-64.763 -72.035)" fill="none" stroke="#F00"> <path d="m73.714 90.274a9.4037 9.4037 0 0 1 9.4674-9.2867 9.4037 9.4037 0 0 1 9.3392 9.4156 9.4037 9.4037 0 0 1-9.3634 9.3915 9.4037 9.4037 0 0 1-9.4435-9.311" stop-color="#000000" style="paint-order:stroke fill markers"/> <path d="m83.118 72.035v36.71" stroke-width=".3"/> <path d="m101.47 90.39h-36.71" stroke-width=".3"/> </g> </svg> </div> </div> </div> <fieldset> <legend>Head positions</legend> <table> <thead> <tr> <th>Head # <button class="btn" onclick="addHead()">Add head</button></th> <th>X</th> <th>Y</th> <th>dX</th> <th>dY</th> </tr> </thead> <tbody> <tr> <th>1</th> <td><input type="number" id="x1" value="0" /></td> <td><input type="number" id="y1" value="0" /></td> <td colspan="2"> With precision: <input type="number" id="precision" value="1" min="0" max="3"/> </td> </tr> <tr class="template"> <th>2</th> <td><input type="number" class="x" value="0" /></td> <td><input type="number" class="y" value="0" /></td> <td><input type="number" readonly class="dx" value="0" /></td> <td><input type="number" readonly class="dy" value="0" /></td> </tr> </tbody> </table> </fieldset> <div style="position:absolute;bottom:10px;left:10px;z-index:9;"> <p><span id="remote-req" class="invisible" style="padding: .3rem 1rem;color: darkblue; background-color: aquamarine;">Request Sent</span></p> </div> <script type="text/javascript"> var sizes = { "240p" : [320, 240], "480p" : [640, 480], "720p" : [960, 720], "FHD 720p" : [1280, 720], "FHD 1080p" : [1920, 1080], }; const dummy = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzcuNTk3IiBoZWlnaHQ9IjM2LjY3MyIgdmlld0JveD0iMCAwIDIwLjUzMSA5LjcwMyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNi45OTYgMS4zMjUgNS4xNiA0Ljc1NWwxLjg5NyAzLjYyM0g0Ljg1bC0uODY4LTEuNzEtLjczNCAxLjcxaC0yLjE3bDEuOTM4LTMuNTY1LTEuODM1LTMuNDg4aDIuMTg2bC44MjIgMS42MDIuNjYxLTEuNjAyeiIgZmlsbD0idGVhbCIvPjxwYXRoIGQ9Ik0xMC40MzMgMS41ODNxMS4zMjIgMCAyLjI2Ny45NjIuOTQ4Ljk2Mi45NDggMi4zMDQgMCAxLjM2My0uOTUzIDIuMzE3LS45NTMuOTUzLTIuMzE3Ljk1My0xLjM1NSAwLTIuMzEzLS45NTgtLjk1OC0uOTU4LS45NTgtMi4zMTIgMC0xLjM2OS45NjMtMi4zMTguOTY3LS45NDggMi4zNjMtLjk0OHptLS4wNTUgMS44MzNxLS41NyAwLS45NzIuNDJ0LS40MDEgMS4wMTNxMCAuNTkzLjQwMSAxLjAxMi40MDYuNDIuOTcyLjQyLjU3NSAwIC45NzEtLjQxNS40MDItLjQyLjQwMi0xLjAxNyAwLS41OTgtLjQwMi0xLjAxMy0uMzk2LS40Mi0uOTcxLS40MnoiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIuMjM0Ii8+PHBhdGggZD0iTTE5LjQ1MyAxLjMyNSAxNy40NTggNC40NnYzLjkxN0gxNS4zNlY0LjQ2MmwtMi4wMS0zLjEzNmgyLjEyOWwuOTEgMS40NTIuOTc2LTEuNDUyeiIgZmlsbD0ibWFyb29uIi8+PHBhdGggZmlsbD0ibm9uZSIgc3R5bGU9InBhaW50LW9yZGVyOnN0cm9rZSBmaWxsIG1hcmtlcnMiIGQ9Ik0wIDBoMjAuNTMxdjkuNzAzSDB6Ii8+PC9zdmc+"; function addHead() { let row = document.querySelector('.template').cloneNode(true); row.classList.remove('template'); document.querySelector('tbody').appendChild(row); let headNum = document.querySelectorAll('tbody tr').length; row.querySelector('th').textContent = headNum; row.querySelectorAll('input').forEach((el) => { el.value = 0; if(!el.readOnly) el.addEventListener('change', updateDeltas); }); } function updateDeltas() { let x1 = document.getElementById('x1').value; let y1 = document.getElementById('y1').value; document.querySelectorAll('tbody tr:not(:first-child)').forEach((row) => { let x = row.querySelector('input.x').value; let y = row.querySelector('input.y').value; let precision = document.getElementById('precision').value; row.querySelector('input.dx').value = (x1 - x).toFixed(precision); row.querySelector('input.dy').value = (y1 - y).toFixed(precision); }); } class DroidCamXView { source = ''; feedimg = null; currimg = null; Xflip = 0; Yflip = 0; aspect = 1; angle = 0; constructor() { this.feedimg = document.getElementById('feedimg'); this.currimg = new Image(640,480); this.currimg.src = dummy; this.feedimg.appendChild(this.currimg); this.cursz = (typeof(Storage) !== "undefined" && localStorage.cursz && sizes[localStorage.cursz]) ? localStorage.cursz : ((window.screen.width < 640) ? "240p" : "480p"); this.setSource( (typeof(Storage) !== "undefined" && localStorage.source) || this.askDroid()); } setSource(source) { if(!source) return; if(!(source.startsWith("http://") || source.startsWith("https://"))) source = (window.location.startsWith("https:") ? "https://" : "http://") + source; this.source = source; if(typeof(Storage) !== "undefined") { localStorage.setItem("source", this.source); } } reload() { if(!this.source) this.setSource(this.askDroid()); this.exec('/override'); this.hidectls(); setTimeout(() => this.startfeed(),2500); } resize(d) { const s = Object.keys(sizes); const c = s.indexOf(this.cursz); const nc = c + d; if(0 <= nc && s.length-1 >= nc) this.setCurrentSize(s[nc]); } setCurrentSize(size) { this.cursz = size; this.reload(); } startfeed() { const oldImg = this.feedimg.querySelector('img'); if (oldImg) this.feedimg.removeChild(oldImg); this.feedimg.style.width = ""; this.feedimg.style.height = ""; const w = sizes[this.cursz][0]; const h = sizes[this.cursz][1]; this.aspect = w / h; this.currimg = new Image(w, h); this.currimg.src = this.source + "/video?" + w + "x" + h; this.currimg.onload = this.showctls; this.currimg.onerror = this.hidectls; this.feedimg.appendChild(this.currimg); if(typeof(Storage) !== "undefined") { localStorage.setItem("cursz", this.cursz); } } // valid option values are: auto, incandescent, warm-fluorescent, twilight, fluorescent, daylight, cloudy-daylight, shade set_wb(el, option) { return this.exec('cam/1/set_wb/' + option) } lockExposure() { return this.exec('/cam/1/set_exposure_lock') } unlockExposure() { return this.exec('/cam/1/unset_exposure_lock') } setAutoFocus() { return this.exec('/cam/1/af') } limitFPS() { return this.exec('/cam/1/fpslimit'); } toggleLed() { return this.exec('/cam/1/led_toggle') } zoomIn() { return this.exec('/cam/1/zoomin') } zoomOut() { return this.exec('/cam/1/zoomout') } flipX() { this.Xflip = !this.Xflip; this.setFlip(); } flipY() { this.Yflip = !this.Yflip; this.setFlip(); } setColor(color) { document.getElementById('x-container').style.borderColor = color; document.getElementById('cross').style.stroke=color } setDroid(){ let droid = this.askDroid(); if (droid != null) { this.setSource(droid); this.reload(); } } askDroid = () => prompt("Enter DroidCamX address", this.source); setFlip = () => this.feedimg.style.transform = `scaleX(${this.Xflip?-1:1}) scaleY(${this.Yflip?-1:1})`; imgWidth = () => this.currimg.offsetWidth ?? this.currimg.naturalWidth; imgHeight = () => this.currimg.offsetHeight ?? this.currimg.naturalHeight; showctls = () => { document.getElementById('button-bar').style.display = 'flex'; document.getElementById('get-droid').remove(); } hidectls = () => { this.currimg.onload = null; this.currimg.src = dummy; document.getElementById('button-bar').style.display = 'none'; } showRemote = () => { let el = document.getElementById('remote-req'); el.classList.remove('invisible'); setTimeout(function(){ el.classList.add('invisible') }, 2200); } exec = (cmd) => { let res = fetch( this.source + cmd, { method: 'GET', mode: "no-cors" }) .catch(() => this.hidectls()) ; this.showRemote(); return res; } } let feed = new DroidCamXView(); document.addEventListener('DOMContentLoaded', () => { feed.startfeed(); document.querySelectorAll('table input').forEach((el) => { el.addEventListener('change', updateDeltas); }); }); </script> </body></html>