Files
crumbmissions/crumbblocks/solar_kettle_dark.html
Branko May Trinkwald 08dd5605a8 fun in the sun <3
2025-12-23 22:25:11 +01:00

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>