(function () { "use strict"; var STORAGE_KEY = "webos-reference-lab-state"; var focusables = []; var focusedIndex = 0; var logItems = []; var networkSubscribed = false; var lastNetworkStatus = null; var canvas = document.getElementById("motionCanvas"); var ctx = canvas.getContext("2d"); var sceneNames = ["Aurora Signal", "Ember Orbit", "Orchid Pulse"]; var themes = ["theme-aurora", "theme-ember", "theme-orchid"]; var state = readStorage(); var lastFrame = 0; var requestFrame = window.requestAnimationFrame || function (callback) { return window.setTimeout(function () { callback(new Date().getTime()); }, 16); }; function $(id) { return document.getElementById(id); } function readStorage() { var fallback = { scene: 0, theme: 0, opens: 0, lastAction: "Fresh install" }; try { var raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) { return fallback; } var parsed = JSON.parse(raw); fallback.scene = Number(parsed.scene) || 0; fallback.theme = Number(parsed.theme) || 0; fallback.opens = Number(parsed.opens) || 0; fallback.lastAction = parsed.lastAction || fallback.lastAction; return fallback; } catch (error) { return fallback; } } function writeStorage() { try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (error) { addLog("Storage", "localStorage unavailable"); } } function setText(id, value) { var node = $(id); if (node) { node.textContent = value; } } function compactJson(value) { try { return JSON.stringify(value, null, 2); } catch (error) { return String(value); } } function setDetail(title, payload) { setText("detailTitle", title); setText("detailJson", compactJson(payload)); } function addLog(label, message) { var stamp = new Date(); var time = two(stamp.getHours()) + ":" + two(stamp.getMinutes()) + ":" + two(stamp.getSeconds()); logItems.unshift({ label: label, message: message, time: time }); logItems = logItems.slice(0, 4); renderLog(); } function renderLog() { var log = $("eventLog"); var html = ""; var i; for (i = 0; i < logItems.length; i += 1) { html += '
' + escapeHtml(logItems[i].time + " " + logItems[i].label) + ' ' + escapeHtml(logItems[i].message) + "
"; } log.innerHTML = html; } function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function two(value) { value = Number(value) || 0; return value < 10 ? "0" + value : String(value); } function applyTheme() { var i; for (i = 0; i < themes.length; i += 1) { document.body.classList.remove(themes[i]); } state.scene = state.scene % sceneNames.length; state.theme = state.theme % themes.length; document.body.classList.add(themes[state.theme]); setText("sceneLabel", sceneNames[state.scene]); } function initFocus() { focusables = Array.prototype.slice.call(document.querySelectorAll("[data-focusable]")); focusables.forEach(function (node, index) { node.addEventListener("focus", function () { focusCard(index); }); node.addEventListener("click", function () { focusCard(index); runAction(node.getAttribute("data-action")); }); }); focusCard(0); } function focusCard(index) { var i; if (!focusables.length) { return; } if (index < 0) { index = focusables.length - 1; } if (index >= focusables.length) { index = 0; } focusedIndex = index; for (i = 0; i < focusables.length; i += 1) { focusables[i].classList.toggle("is-focused", i === focusedIndex); } if (document.activeElement !== focusables[focusedIndex]) { focusables[focusedIndex].focus(); } } function moveFocus(direction) { var current = focusables[focusedIndex]; var currentRect; var currentCenter; var bestIndex = focusedIndex; var bestScore = Infinity; var i; if (!current) { return; } currentRect = current.getBoundingClientRect(); currentCenter = centerOf(currentRect); for (i = 0; i < focusables.length; i += 1) { var candidate = focusables[i]; var rect = candidate.getBoundingClientRect(); var center = centerOf(rect); var dx = center.x - currentCenter.x; var dy = center.y - currentCenter.y; var primary; var secondary; var score; if (i === focusedIndex) { continue; } if (direction === "left" && dx >= -4) { continue; } if (direction === "right" && dx <= 4) { continue; } if (direction === "up" && dy >= -4) { continue; } if (direction === "down" && dy <= 4) { continue; } primary = direction === "left" || direction === "right" ? Math.abs(dx) : Math.abs(dy); secondary = direction === "left" || direction === "right" ? Math.abs(dy) : Math.abs(dx); score = primary * 8 + secondary; if (score < bestScore) { bestScore = score; bestIndex = i; } } if (bestIndex !== focusedIndex) { focusCard(bestIndex); } } function centerOf(rect) { return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; } function handleKeydown(event) { var code = event.keyCode || event.which; var keyName = event.key || String(code); setText("keyValue", keyName); if (code === 37) { event.preventDefault(); moveFocus("left"); } else if (code === 38) { event.preventDefault(); moveFocus("up"); } else if (code === 39) { event.preventDefault(); moveFocus("right"); } else if (code === 40) { event.preventDefault(); moveFocus("down"); } else if (code === 13) { event.preventDefault(); runAction(focusables[focusedIndex].getAttribute("data-action")); } else if (code === 461 || code === 10009 || code === 27) { event.preventDefault(); handleBack(); } else if (code === 403) { cycleScene(); } else if (code === 404) { refreshAll(); } else if (code === 405) { runAction("storage"); } else if (code === 406) { logItems = []; renderLog(); } } function handleBack() { addLog("Back", "platformBack requested"); if (window.webOS && typeof window.webOS.platformBack === "function") { window.webOS.platformBack(); } } function runAction(action) { state.lastAction = action; if (action === "clock") { getClock(); } else if (action === "device") { getDeviceInfo(); } else if (action === "network") { getNetworkStatus(); } else if (action === "launch") { launchBrowser(); } else if (action === "storage") { saveLocalState(); } else if (action === "motion") { cycleScene(); } writeStorage(); } function refreshAll() { loadAppInfo(); loadLaunchParams(); getClock(); getDeviceInfo(); getNetworkStatus(); getLGUDID(); addLog("Refresh", "runtime probes requested"); } function loadAppInfo() { var fallback = { id: window.webOS && window.webOS.fetchAppId ? window.webOS.fetchAppId() : "browser.preview", title: "webOS TV Reference Lab", root: window.webOS && window.webOS.fetchAppRootPath ? window.webOS.fetchAppRootPath() : window.location.href }; if (window.webOS && typeof window.webOS.fetchAppInfo === "function") { window.webOS.fetchAppInfo(function (info) { var payload = info || fallback; payload.root = fallback.root; setText("appTitle", payload.title || payload.id || "App"); setDetail("App Info", payload); addLog("App", "appinfo.json loaded"); }); } else { setText("appTitle", fallback.title); setDetail("App Info", fallback); } setText("platformValue", platformLabel()); } function platformLabel() { var platform = window.webOS && window.webOS.platform ? window.webOS.platform : {}; var labels = []; var key; for (key in platform) { if (platform.hasOwnProperty(key) && platform[key]) { labels.push(key); } } if (window.PalmSystem && window.PalmSystem.deviceInfo) { labels.push("PalmSystem"); } return labels.length ? labels.join(", ") : "Browser"; } function loadLaunchParams() { var params = {}; if (window.webOSDev && typeof window.webOSDev.launchParams === "function") { params = window.webOSDev.launchParams(); } if (!params || Object.keys(params).length === 0) { params = { mode: "preview", scene: sceneNames[state.scene], opens: state.opens }; } setText("launchParams", compactJson(params)); } function getClock() { if (window.webOS && window.webOS.service) { window.webOS.service.request("luna://com.palm.systemservice", { method: "clock/getTime", parameters: {}, onSuccess: function (args) { var payload = { utc: args.utc, localtime: args.localtime, timezone: args.timezone, source: "LS2" }; setText("clockValue", formatNow()); setDetail("System Clock", payload); addLog("Clock", "LS2 response received"); }, onFailure: function (args) { useBrowserClock(args); } }); } else { useBrowserClock({ errorText: "PalmServiceBridge is not available" }); } } function useBrowserClock(error) { var payload = { local: new Date().toString(), timezone: window.webOS && window.webOS.systemInfo ? window.webOS.systemInfo().timezone : "browser", source: "Browser fallback", error: error && (error.errorText || error.errorCode) }; setText("clockValue", formatNow()); setDetail("System Clock", payload); addLog("Clock", "browser fallback used"); } function formatNow() { var now = new Date(); return two(now.getHours()) + ":" + two(now.getMinutes()); } function getDeviceInfo() { if (window.webOS && typeof window.webOS.deviceInfo === "function") { window.webOS.deviceInfo(function (info) { var payload = info || {}; payload.webOSTVjs = window.webOS.libVersion || "unknown"; setDetail("Device Info", payload); addLog("Device", payload.modelName || "device info updated"); }); } else { setDetail("Device Info", { userAgent: window.navigator.userAgent, screen: window.screen.width + "x" + window.screen.height, webOSTVjs: "not loaded" }); addLog("Device", "browser info shown"); } } function getNetworkStatus() { if (networkSubscribed) { if (lastNetworkStatus) { setDetail("Connection", lastNetworkStatus); } addLog("Network", "subscription already active"); return; } networkSubscribed = true; if (window.webOSDev && window.webOSDev.connection) { window.webOSDev.connection.getStatus({ subscribe: true, onSuccess: function (status) { var online = status.isInternetConnectionAvailable || status.wired || status.wifi || window.navigator.onLine; lastNetworkStatus = status; setText("networkValue", online ? "Online" : "Offline"); setDetail("Connection", status); addLog("Network", "connection status updated"); }, onFailure: function (error) { networkSubscribed = false; useBrowserNetwork(error); } }); } else { networkSubscribed = false; useBrowserNetwork({ errorText: "webOSDev.connection is not available" }); } } function useBrowserNetwork(error) { var payload = { onLine: window.navigator.onLine, source: "Browser fallback", error: error && (error.errorText || error.errorCode) }; lastNetworkStatus = payload; setText("networkValue", payload.onLine ? "Online" : "Offline"); setDetail("Connection", payload); addLog("Network", "browser status shown"); } function getLGUDID() { if (window.webOSDev && typeof window.webOSDev.LGUDID === "function") { window.webOSDev.LGUDID({ onSuccess: function (payload) { addLog("LGUDID", maskId(payload.id)); }, onFailure: function () { addLog("LGUDID", "not available in this runtime"); } }); } } function maskId(id) { if (!id || id.length < 8) { return "hidden"; } return id.slice(0, 4) + "..." + id.slice(id.length - 4); } function launchBrowser() { var target = "https://webostv.developer.lge.com/"; var payload = { id: "com.webos.app.browser", target: target }; setDetail("Launch Browser", payload); if (window.webOSDev && typeof window.webOSDev.launch === "function" && window.PalmServiceBridge) { window.webOSDev.launch({ id: window.webOSDev.APP.BROWSER, params: { target: target }, onSuccess: function () { addLog("Launch", "browser launch requested"); }, onFailure: function (error) { addLog("Launch", error.errorText || "launch failed"); } }); } else { window.open(target, "_blank"); addLog("Launch", "window.open fallback"); } } function saveLocalState() { state.opens += 1; state.theme = (state.theme + 1) % themes.length; state.lastAction = "storage"; applyTheme(); writeStorage(); loadLaunchParams(); setDetail("Local State", { key: STORAGE_KEY, value: state }); addLog("Storage", "state saved"); } function cycleScene() { state.scene = (state.scene + 1) % sceneNames.length; state.theme = state.scene; applyTheme(); writeStorage(); loadLaunchParams(); setDetail("Canvas Scene", { scene: sceneNames[state.scene], canvas: canvas.width + "x" + canvas.height, animation: "requestAnimationFrame" }); addLog("Canvas", sceneNames[state.scene]); } function resizeCanvas() { var rect = canvas.getBoundingClientRect(); var ratio = window.devicePixelRatio || 1; var width = Math.max(320, Math.floor(rect.width * ratio)); var height = Math.max(180, Math.floor(rect.height * ratio)); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } function drawFrame(timestamp) { paintCanvas(timestamp); requestFrame(drawFrame); } function paintCanvas(timestamp) { var width; var height; var i; var t; if (!lastFrame) { lastFrame = timestamp; } resizeCanvas(); width = canvas.width; height = canvas.height; t = timestamp * 0.001; ctx.clearRect(0, 0, width, height); drawBackdrop(width, height, t); for (i = 0; i < 9; i += 1) { drawOrb(width, height, t, i); } drawBars(width, height, t); lastFrame = timestamp; } function drawBackdrop(width, height, t) { var gradient = ctx.createLinearGradient(0, 0, width, height); if (state.scene === 1) { gradient.addColorStop(0, "#2a1710"); gradient.addColorStop(0.55, "#11161b"); gradient.addColorStop(1, "#293227"); } else if (state.scene === 2) { gradient.addColorStop(0, "#1b112a"); gradient.addColorStop(0.5, "#101620"); gradient.addColorStop(1, "#203226"); } else { gradient.addColorStop(0, "#0b1720"); gradient.addColorStop(0.55, "#11141a"); gradient.addColorStop(1, "#1d2b21"); } ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); ctx.globalAlpha = 0.18; ctx.fillStyle = "#ffffff"; for (var i = 0; i < 7; i += 1) { var y = (height / 7) * i + Math.sin(t + i) * 18; ctx.fillRect(0, y, width, 1); } ctx.globalAlpha = 1; } function drawOrb(width, height, t, index) { var colors = [ ["#6ce3cf", "#f3cf56"], ["#ff7c67", "#83d96c"], ["#c49cff", "#6aa9ff"] ]; var palette = colors[state.scene]; var radius = width * (0.035 + (index % 3) * 0.012); var x = width * (0.12 + index * 0.095) + Math.sin(t * (0.7 + index * 0.05)) * width * 0.045; var y = height * (0.48 + Math.sin(t * 0.8 + index) * 0.27); var gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); gradient.addColorStop(0, palette[index % 2]); gradient.addColorStop(1, "rgba(255,255,255,0)"); ctx.globalAlpha = 0.78; ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } function drawBars(width, height, t) { var x = width * 0.64; var y = height * 0.22; var barWidth = width * 0.035; var gap = width * 0.018; var i; for (i = 0; i < 7; i += 1) { var level = 0.22 + Math.abs(Math.sin(t * 1.5 + i * 0.7)) * 0.56; ctx.fillStyle = i % 2 ? "rgba(255,255,255,0.26)" : "rgba(155,231,211,0.62)"; ctx.fillRect(x + i * (barWidth + gap), y + height * (0.52 - level), barWidth, height * level); } } function init() { state.opens += 1; applyTheme(); writeStorage(); initFocus(); loadAppInfo(); loadLaunchParams(); getClock(); getNetworkStatus(); getDeviceInfo(); window.addEventListener("keydown", handleKeydown); window.addEventListener("online", function () { useBrowserNetwork(); }); window.addEventListener("offline", function () { useBrowserNetwork(); }); paintCanvas(0); requestFrame(drawFrame); addLog("Ready", "reference lab started"); } window.onerror = function (message) { addLog("Error", message); }; init(); }());