687 lines
21 KiB
HTML
687 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>☀️ Solar Wasserkocher</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<script src="lib/blockly/blockly.min.js"></script>
|
|
<script src="lib/blockly/javascript.min.js"></script>
|
|
<script src="lib/blockly/de.js"></script>
|
|
<style>
|
|
:root {
|
|
--bg: #05140a;
|
|
/* Deep Forest Black */
|
|
--panel: #0d2615;
|
|
/* Dark Moss */
|
|
--txt: #e0f2f1;
|
|
/* Mint White */
|
|
--accent: #00e676;
|
|
/* Vibrant Mint */
|
|
--accent-hover: #b9f6ca;
|
|
--border: #1b5e20;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--txt);
|
|
font-family: 'Segoe UI', sans-serif;
|
|
height: 100vh;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
header {
|
|
padding: 15px 20px;
|
|
background: rgba(13, 38, 21, 0.95);
|
|
border-bottom: 2px solid var(--accent);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
box-shadow: 0 4px 20px rgba(0, 230, 118, 0.1);
|
|
}
|
|
|
|
h2 {
|
|
margin: 0;
|
|
flex: 1;
|
|
font-size: 1.2rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: var(--accent);
|
|
}
|
|
|
|
button {
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
border: 1px solid var(--accent);
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
color: var(--accent);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
button:hover {
|
|
background: rgba(0, 230, 118, 0.1);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 2px 10px rgba(0, 230, 118, 0.2);
|
|
}
|
|
|
|
button.primary {
|
|
background: var(--accent);
|
|
color: #003d15;
|
|
border: none;
|
|
}
|
|
|
|
button.primary:hover {
|
|
background: var(--accent-hover);
|
|
color: #000;
|
|
box-shadow: 0 0 15px var(--accent);
|
|
}
|
|
|
|
button.action {
|
|
border-color: #ff9800;
|
|
color: #ff9800;
|
|
}
|
|
|
|
button.action:hover {
|
|
background: rgba(255, 152, 0, 0.1);
|
|
box-shadow: 0 2px 10px rgba(255, 152, 0, 0.2);
|
|
}
|
|
|
|
#main {
|
|
flex: 1;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#blocklyDiv {
|
|
flex: 3;
|
|
border-right: 1px solid var(--border);
|
|
}
|
|
|
|
#simDiv {
|
|
flex: 2;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
background: var(--panel);
|
|
position: relative;
|
|
}
|
|
|
|
/* Visualization */
|
|
#vis {
|
|
width: 100%;
|
|
height: 300px;
|
|
background: linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 100%);
|
|
border-radius: 10px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Elements */
|
|
.sun {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: 60px;
|
|
height: 60px;
|
|
background: #FFD700;
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 40px #FFD700;
|
|
transition: all 0.5s;
|
|
}
|
|
|
|
.cloud {
|
|
position: absolute;
|
|
top: 40px;
|
|
left: -100px;
|
|
width: 120px;
|
|
height: 60px;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
border-radius: 30px;
|
|
transition: left 2s linear;
|
|
filter: blur(5px);
|
|
}
|
|
|
|
.kettle-base {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 120px;
|
|
height: 160px;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border: 2px solid #555;
|
|
border-radius: 10px 10px 20px 20px;
|
|
backdrop-filter: blur(2px);
|
|
z-index: 10;
|
|
}
|
|
|
|
.water {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 20%;
|
|
background: #2196F3;
|
|
opacity: 0.8;
|
|
border-radius: 0 0 18px 18px;
|
|
transition: height 0.5s, background-color 0.5s;
|
|
}
|
|
|
|
.bubbles {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: none;
|
|
}
|
|
|
|
.bubble {
|
|
position: absolute;
|
|
background: rgba(255, 255, 255, 0.5);
|
|
border-radius: 50%;
|
|
animation: rise 2s infinite;
|
|
}
|
|
|
|
@keyframes rise {
|
|
0% {
|
|
bottom: 0;
|
|
opacity: 0;
|
|
}
|
|
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
|
|
100% {
|
|
bottom: 100%;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.panel {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
width: 60px;
|
|
height: 80px;
|
|
background: #111;
|
|
border: 2px solid #666;
|
|
transform: skewX(-10deg);
|
|
}
|
|
|
|
.wire {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 80px;
|
|
width: 80px;
|
|
height: 5px;
|
|
background: #333;
|
|
transform: rotate(-5deg);
|
|
z-index: 5;
|
|
}
|
|
|
|
.info-overlay {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
#log {
|
|
flex: 1;
|
|
background: #111;
|
|
color: #0f0;
|
|
font-family: monospace;
|
|
padding: 10px;
|
|
overflow-y: auto;
|
|
border-radius: 5px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.heating .kettle-base {
|
|
box-shadow: 0 0 20px red;
|
|
border-color: red;
|
|
}
|
|
|
|
/* === BLOCKLY DEEP FOREST OVERRIDES === */
|
|
.blocklySvg {
|
|
background-color: var(--bg) !important;
|
|
}
|
|
|
|
/* Toolbox (Sidebar) - Dark Grey High Contrast */
|
|
.blocklyToolboxDiv {
|
|
background-color: #121212 !important;
|
|
color: #eeeeee !important;
|
|
border-right: 1px solid var(--border);
|
|
}
|
|
|
|
/* Category Labels */
|
|
.blocklyTreeLabel {
|
|
font-family: 'Segoe UI', sans-serif !important;
|
|
color: #bbbbbb !important;
|
|
}
|
|
|
|
/* Rows */
|
|
.blocklyTreeRow {
|
|
border-left: 4px solid transparent;
|
|
margin-bottom: 2px !important;
|
|
line-height: 24px !important;
|
|
}
|
|
|
|
/* Hover */
|
|
.blocklyTreeRow:hover {
|
|
background-color: #1e1e1e !important;
|
|
border-left-color: var(--accent-hover);
|
|
}
|
|
|
|
/* Selected */
|
|
.blocklyTreeSelected .blocklyTreeRow {
|
|
background-color: #0d2615 !important;
|
|
/* Moss background for active */
|
|
border-left-color: var(--accent) !important;
|
|
}
|
|
|
|
.blocklyTreeSelected .blocklyTreeLabel {
|
|
color: var(--accent) !important;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Flyout (Popup) */
|
|
.blocklyFlyoutBackground {
|
|
fill: #181818 !important;
|
|
fill-opacity: 0.98 !important;
|
|
}
|
|
|
|
/* Text in Flyout headers */
|
|
.blocklyFlyoutLabelText {
|
|
fill: var(--txt) !important;
|
|
}
|
|
|
|
/* Scrollbars */
|
|
.blocklyScrollbarHandle {
|
|
fill: var(--accent) !important;
|
|
fill-opacity: 0.5 !important;
|
|
}
|
|
|
|
/* === DROPDOWNS & MENUS (The "Helle Overlay" Fix) === */
|
|
.blocklyDropDownDiv,
|
|
.blocklyWidgetDiv .goog-menu,
|
|
.goog-menu {
|
|
background-color: #121212 !important;
|
|
border: 1px solid var(--accent) !important;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.8) !important;
|
|
border-radius: 4px !important;
|
|
}
|
|
|
|
/* Content Text in Menus */
|
|
.goog-menuitem-content,
|
|
.goog-menuitem,
|
|
.blocklyDropDownDiv .goog-menuitem-content {
|
|
color: #eeeeee !important;
|
|
background-color: transparent !important;
|
|
font-family: 'Segoe UI', sans-serif !important;
|
|
}
|
|
|
|
/* Hover State in Menus */
|
|
.goog-menuitem-highlight,
|
|
.goog-menuitem:hover {
|
|
background-color: rgba(0, 230, 118, 0.2) !important;
|
|
border-color: transparent !important;
|
|
}
|
|
|
|
/* Input Fields (Number editing) */
|
|
.blocklyHtmlInput {
|
|
background-color: #05140a !important;
|
|
color: var(--accent) !important;
|
|
border: 1px solid var(--accent) !important;
|
|
border-radius: 4px !important;
|
|
font-family: monospace !important;
|
|
padding: 2px 5px !important;
|
|
}
|
|
|
|
/* Tooltips */
|
|
.blocklyTooltipDiv {
|
|
background-color: #0d2615 !important;
|
|
color: var(--txt) !important;
|
|
border: 1px solid var(--accent) !important;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5) !important;
|
|
}
|
|
|
|
/* === FINAL UI POLISH === */
|
|
#main {
|
|
border: 2px solid #4fc3f7 !important;
|
|
/* Light Blue Accent */
|
|
box-shadow: 0 0 15px rgba(79, 195, 247, 0.1);
|
|
}
|
|
|
|
/* Force Toolbox Background deeper */
|
|
.blocklyToolboxContents,
|
|
.blocklyTreeRoot,
|
|
.blocklyToolbox {
|
|
background-color: #121212 !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<header>
|
|
<h2>☀️ Solar-Wasserkocher Sim</h2>
|
|
<span id="status" style="font-size: 0.9rem; color: #aaa;">Bereit.</span>
|
|
<button onclick="runCode()" class="primary">▶️ Starten</button>
|
|
<button onclick="resetSim()">🔄 Reset</button>
|
|
<button onclick="exportData()" class="action">🚀 An Crew senden</button>
|
|
</header>
|
|
|
|
<div id="main">
|
|
<div id="blocklyDiv"></div>
|
|
<div id="simDiv">
|
|
<div id="vis">
|
|
<div class="sun" id="sun"></div>
|
|
<div class="cloud" id="cloud"></div>
|
|
<div class="panel"></div>
|
|
<div class="wire"></div>
|
|
<div class="kettle-base" id="kettle">
|
|
<div class="water" id="water"></div>
|
|
<div class="bubbles" id="bubbles"></div>
|
|
</div>
|
|
<div class="info-overlay">
|
|
Temp: <span id="valTemp">20.0</span> °C<br>
|
|
Power: <span id="valPower">0</span> W<br>
|
|
Energie: <span id="valEnergy">0</span> kJ<br>
|
|
Zeit: <span id="valTime">0</span> s
|
|
</div>
|
|
</div>
|
|
<div id="log">Logs:<br></div>
|
|
</div>
|
|
</div>
|
|
|
|
<xml id="toolbox" style="display: none">
|
|
<category name="Sensoren" colour="180">
|
|
<block type="sensor_temp"></block>
|
|
<block type="sensor_power"></block>
|
|
</category>
|
|
<category name="Aktor" colour="30">
|
|
<block type="heater_switch"></block>
|
|
<block type="wait_seconds"></block>
|
|
</category>
|
|
<category name="Logik" colour="120">
|
|
<block type="controls_if"></block>
|
|
<block type="logic_compare"></block>
|
|
<block type="logic_operation"></block>
|
|
<block type="logic_boolean"></block>
|
|
</category>
|
|
<category name="Schleifen" colour="150">
|
|
<block type="controls_whileUntil"></block>
|
|
</category>
|
|
<category name="Mathe" colour="200">
|
|
<block type="math_number"></block>
|
|
<block type="math_arithmetic"></block>
|
|
</category>
|
|
<category name="Variablen" custom="VARIABLE" colour="90"></category>
|
|
</xml>
|
|
|
|
<script>
|
|
// === PHYSICS ENGINE ===
|
|
const SIM = {
|
|
running: false,
|
|
time: 0,
|
|
dt: 0.1, // 100ms steps
|
|
cloudPos: -100,
|
|
|
|
// State
|
|
temp: 20.0,
|
|
volume: 1.0, // Liter (kg)
|
|
heaterOn: false,
|
|
energyUsed: 0.0, // kJ
|
|
|
|
// Constants
|
|
c: 4.184, // kJ/(kg*K) specific heat water
|
|
maxPower: 2000, // Watt at full sun
|
|
lossFactor: 0.05, // Cooling per degree delta T
|
|
|
|
// External
|
|
sunIntensity: 1.0, // 0..1
|
|
|
|
reset() {
|
|
this.running = false;
|
|
this.time = 0;
|
|
this.temp = 20.0;
|
|
this.heaterOn = false;
|
|
this.energyUsed = 0.0;
|
|
this.sunIntensity = 1.0;
|
|
this.cloudPos = -100;
|
|
updateVis();
|
|
log("Sim reset.");
|
|
},
|
|
|
|
step() {
|
|
if (!this.running) return;
|
|
|
|
// Environment logic
|
|
this.time += this.dt;
|
|
this.cloudPos += 0.5;
|
|
if (this.cloudPos > 400) this.cloudPos = -150;
|
|
|
|
// Sun logic (cloud blocks sun)
|
|
const cloudCenter = this.cloudPos + 60;
|
|
const panelCenter = 40; // Approx css left
|
|
let dist = Math.abs(cloudCenter - panelCenter);
|
|
this.sunIntensity = (dist < 80) ? 0.2 : 1.0;
|
|
|
|
// Physics
|
|
const currentPowerW = this.heaterOn ? (this.maxPower * this.sunIntensity) : 0;
|
|
const heatingKJ = (currentPowerW / 1000) * this.dt;
|
|
|
|
const deltaT_heat = heatingKJ / (this.volume * this.c);
|
|
const deltaT_cool = (this.temp - 20) * this.lossFactor * this.dt;
|
|
|
|
this.temp += (deltaT_heat - deltaT_cool);
|
|
this.energyUsed += heatingKJ;
|
|
|
|
if (this.temp > 100) this.temp = 100; // Cap at boiling (energy wasted phase) -> actually lets cap valid boiling, but track waste?
|
|
// Let's cap at 120 for "Explosion" visualization? kept simple for now.
|
|
|
|
updateVis();
|
|
}
|
|
};
|
|
|
|
function updateVis() {
|
|
document.getElementById('valTemp').innerText = SIM.temp.toFixed(1);
|
|
document.getElementById('valPower').innerText = SIM.heaterOn ? (SIM.maxPower * SIM.sunIntensity).toFixed(0) : "0";
|
|
document.getElementById('valEnergy').innerText = SIM.energyUsed.toFixed(1);
|
|
document.getElementById('valTime').innerText = SIM.time.toFixed(1);
|
|
|
|
// Kettle
|
|
const h = Math.min(SIM.temp, 100);
|
|
document.getElementById('water').style.height = (20 + (h / 100) * 60) + "%"; // Mock level
|
|
|
|
// Color: Blue -> Red
|
|
const r = Math.min(255, (SIM.temp - 20) * 3);
|
|
const b = Math.max(0, 255 - (SIM.temp - 20) * 3);
|
|
document.getElementById('water').style.backgroundColor = `rgb(${r}, 0, ${b})`;
|
|
|
|
// Bubbles
|
|
document.getElementById('bubbles').style.display = (SIM.temp > 95) ? 'block' : 'none';
|
|
|
|
// Glow
|
|
const kettle = document.getElementById('kettle');
|
|
if (SIM.heaterOn) kettle.classList.add('heating'); else kettle.classList.remove('heating');
|
|
|
|
// Cloud
|
|
document.getElementById('cloud').style.left = SIM.cloudPos + "px";
|
|
|
|
// Sun dim
|
|
document.getElementById('sun').style.opacity = SIM.sunIntensity;
|
|
}
|
|
|
|
function log(msg) {
|
|
const l = document.getElementById('log');
|
|
l.innerHTML += `> ${msg}<br>`;
|
|
l.scrollTop = l.scrollHeight;
|
|
}
|
|
|
|
// === BLOCKLY SETUP ===
|
|
Blockly.defineBlocksWithJsonArray([
|
|
{
|
|
"type": "sensor_temp",
|
|
"message0": "🌡️ Temperatur (°C)",
|
|
"output": "Number",
|
|
"colour": 230,
|
|
"tooltip": "Misst die Wassertemperatur"
|
|
},
|
|
{
|
|
"type": "sensor_power",
|
|
"message0": "☀️ Verfügbare Power (W)",
|
|
"output": "Number",
|
|
"colour": 230,
|
|
"tooltip": "Zeigt wie viel Sonnenenergie da ist"
|
|
},
|
|
{
|
|
"type": "heater_switch",
|
|
"message0": "🔥 Heizung %1",
|
|
"args0": [
|
|
{
|
|
"type": "field_dropdown",
|
|
"name": "STATE",
|
|
"options": [["AN", "ON"], ["AUS", "OFF"]]
|
|
}
|
|
],
|
|
"previousStatement": null,
|
|
"nextStatement": null,
|
|
"colour": 160,
|
|
"tooltip": "Schaltet den Heizstab"
|
|
},
|
|
{
|
|
"type": "wait_seconds",
|
|
"message0": "⏳ Warte %1 Sek.",
|
|
"args0": [
|
|
{
|
|
"type": "field_number",
|
|
"name": "SECONDS",
|
|
"value": 1,
|
|
"min": 0.1
|
|
}
|
|
],
|
|
"previousStatement": null,
|
|
"nextStatement": null,
|
|
"colour": 160,
|
|
"tooltip": "Wartet und lässt Zeit vergehen"
|
|
}
|
|
]);
|
|
|
|
const workspace = Blockly.inject('blocklyDiv', {
|
|
toolbox: document.getElementById('toolbox'),
|
|
theme: Blockly.Themes.Dark,
|
|
renderer: 'zelos'
|
|
});
|
|
|
|
// Code Generators
|
|
javascript.javascriptGenerator.forBlock['sensor_temp'] = function (block) {
|
|
return ['getTemp()', javascript.Order.ATOMIC];
|
|
};
|
|
javascript.javascriptGenerator.forBlock['sensor_power'] = function (block) {
|
|
return ['getPower()', javascript.Order.ATOMIC];
|
|
};
|
|
javascript.javascriptGenerator.forBlock['heater_switch'] = function (block) {
|
|
var state = block.getFieldValue('STATE');
|
|
return 'setHeater("' + state + '");\n';
|
|
};
|
|
javascript.javascriptGenerator.forBlock['wait_seconds'] = function (block) {
|
|
var sec = block.getFieldValue('SECONDS');
|
|
return 'await wait(' + sec + ');\n';
|
|
};
|
|
|
|
// === INTERFACE FOR GENERATED CODE ===
|
|
function getTemp() { return SIM.temp; }
|
|
function getPower() { return SIM.sunIntensity * SIM.maxPower; }
|
|
function setHeater(state) {
|
|
SIM.heaterOn = (state === 'ON');
|
|
log(`Heizung: ${state}`);
|
|
updateVis();
|
|
}
|
|
function wait(sec) {
|
|
return new Promise(resolve => setTimeout(resolve, sec * 1000));
|
|
}
|
|
|
|
// Safety Stepper
|
|
async function _step(id) {
|
|
workspace.highlightBlock(id);
|
|
await new Promise(r => setTimeout(r, 10)); // Minimal delay to keep UI alive
|
|
}
|
|
|
|
let loopInterval;
|
|
|
|
async function runCode() {
|
|
resetSim();
|
|
SIM.running = true;
|
|
document.getElementById('status').innerText = "Läuft...";
|
|
|
|
// Start Physics Loop independent of Code
|
|
if (loopInterval) clearInterval(loopInterval);
|
|
loopInterval = setInterval(() => SIM.step(), SIM.dt * 1000); // 100ms real time = 0.1s sim time
|
|
|
|
// Generator Config
|
|
javascript.javascriptGenerator.STATEMENT_PREFIX = 'await _step(%1);\n';
|
|
javascript.javascriptGenerator.addReservedWords('code');
|
|
|
|
var code = javascript.javascriptGenerator.workspaceToCode(workspace);
|
|
|
|
try {
|
|
// Wrap in async function
|
|
const wrappedCode = `(async () => {
|
|
${code}
|
|
log("🏁 Programm Ende");
|
|
SIM.running = false;
|
|
clearInterval(loopInterval);
|
|
document.getElementById('status').innerText = "Fertig.";
|
|
})();`;
|
|
|
|
eval(wrappedCode);
|
|
} catch (e) {
|
|
log("❌ Fehler: " + e);
|
|
SIM.running = false;
|
|
}
|
|
}
|
|
|
|
function resetSim() {
|
|
SIM.reset();
|
|
if (loopInterval) clearInterval(loopInterval);
|
|
document.getElementById('log').innerHTML = "Logs:<br>";
|
|
document.getElementById('status').innerText = "Reset.";
|
|
workspace.highlightBlock(null);
|
|
}
|
|
|
|
async function exportData() {
|
|
const data = {
|
|
mission: "solar_kettle",
|
|
energy_kj: SIM.energyUsed.toFixed(2),
|
|
final_temp: SIM.temp.toFixed(1),
|
|
sim_time: SIM.time.toFixed(1),
|
|
code_summary: "Blockly Code ausgeführt", // Could be more creating
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
const json = JSON.stringify(data, null, 2);
|
|
try {
|
|
await navigator.clipboard.writeText(json);
|
|
alert("Daten kopiert!\n\n" + json + "\n\n-> ./evaluate_mission_data.sh");
|
|
} catch (e) {
|
|
alert("Clipboard Fehler: " + e);
|
|
}
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |