wow thats a lot of changes

This commit is contained in:
2026-01-20 16:50:04 -06:00
parent b0f26c4c6c
commit 9c9c6b99b3
12 changed files with 1679 additions and 112 deletions

View File

@@ -307,6 +307,46 @@
</value> </value>
</block> </block>
<sep gap="50"></sep> <sep gap="50"></sep>
<block type="display_text_as_sprite">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">Hello World!</field>
</shadow>
</value>
</block>
<block type="clear_text_sprite"></block>
<block type="set_text_property">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">Arial</field>
</shadow>
</value>
</block>
<sep gap="50"></sep>
<block type="set_color_effect">
<value name="COLOR">
<shadow type="colour_picker">
<field name="COLOUR">#ff0000</field>
</shadow>
</value>
</block>
<block type="set_effect">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">25</field>
</shadow>
</value>
</block>
<block type="change_effect">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">25</field>
</shadow>
</value>
</block>
<block type="clear_effects"></block>
<block type="switch_costume"> <block type="switch_costume">
<value name="COSTUME"> <value name="COSTUME">
<shadow type="text"> <shadow type="text">
@@ -442,6 +482,28 @@
<block type="window_size"></block> <block type="window_size"></block>
</category> </category>
<category name="Sensing" colour="#4CBFE6">
<block type="touching"></block>
<block type="touching_color"></block>
<block type="mouse_down"></block>
<sep gap="50"></sep>
<block type="distance_to"></block>
<sep gap="50"></sep>
<block type="ask_and_wait">
<value name="QUESTION">
<shadow type="text">
<field name="TEXT">What's your name?</field>
</shadow>
</value>
</block>
<block type="answer"></block>
<sep gap="50"></sep>
<block type="current"></block>
<block type="days_since_2000"></block>
<block type="username"></block>
<block type="loudness"></block>
</category>
<category name="Lists" colour="#e35340"> <category name="Lists" colour="#e35340">
<block type="lists_create_with"> <block type="lists_create_with">
<mutation items="2"></mutation> <mutation items="2"></mutation>
@@ -554,6 +616,12 @@
</category> </category>
<category name="Variables" colour="#FF8C1A" custom="GLOBAL_VARIABLES"></category> <category name="Variables" colour="#FF8C1A" custom="GLOBAL_VARIABLES"></category>
<category name="Variable Tools" colour="#FF8C1A">
<block type="show_variable"></block>
<block type="hide_variable"></block>
<block type="move_variable_to"></block>
</category>
</xml> </xml>
<script type="module" src="src/scripts/editor.js"></script> <script type="module" src="src/scripts/editor.js"></script>

101
ext.js
View File

@@ -1,101 +0,0 @@
class TextSpriteExtension {
id = "textSprite";
registerCategory() {
return {
name: "Text Sprite",
color: "#8E44AD",
};
}
registerBlocks() {
return [
{
type: "statement",
id: "createTextSprite",
text: "create text sprite [text] font [font] size [size] color [color]",
tooltip: "Create a new sprite rendered from text",
fields: {
text: { kind: "value", type: "String", default: "Hello!" },
font: {
kind: "menu",
items: [
"Arial",
"Verdana",
"Courier New",
"Times New Roman",
"Comic Sans MS",
],
},
size: { kind: "value", type: "Number", default: "48" },
color: { kind: "value", type: "String", default: "#000000" },
},
},
];
}
registerCode() {
return {
createTextSprite: (inputs) => {
if (!window.app || !window.PIXI) return;
const text = String(inputs.text ?? "");
const font = inputs.font || "Arial";
const size = Number(inputs.size || 48);
const color = String(inputs.color || "#000000");
/* ---------- Canvas ---------- */
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = `${size}px ${font}`;
const metrics = ctx.measureText(text);
canvas.width = Math.ceil(metrics.width) + 20;
canvas.height = size + 20;
ctx.font = `${size}px ${font}`;
ctx.fillStyle = color;
ctx.textBaseline = "top";
ctx.fillText(text, 10, 10);
/* ---------- PIXI Texture ---------- */
const baseTexture = new PIXI.BaseTexture(canvas);
const texture = new PIXI.Texture(baseTexture);
baseTexture.update();
/* ---------- Create Sprite (same as addSprite) ---------- */
const pixiSprite = new PIXI.Sprite(texture);
pixiSprite.anchor.set(0.5);
pixiSprite.x = 0;
pixiSprite.y = 0;
pixiSprite.scale._parentScaleEvent = pixiSprite;
app.stage.addChild(pixiSprite);
const id = "text-sprite-" + Date.now();
const spriteData = {
id,
pixiSprite,
code: "",
costumes: [{ name: "text", texture }],
sounds: [],
};
sprites.push(spriteData);
// Register globally
if (!window.projectCostumes.includes("text")) {
window.projectCostumes.push("text");
}
// Select it
setActiveSprite(spriteData);
renderSpritesList(true);
},
};
}
}
registerExtension(TextSpriteExtension);

44
package-lock.json generated
View File

@@ -8,6 +8,8 @@
"name": "rarry-vite", "name": "rarry-vite",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@blockly/field-colour": "^6.0.11",
"@blockly/field-colour-hsv-sliders": "^6.0.11",
"@blockly/plugin-strict-connection-checker": "^6.0.1", "@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0", "blockly": "^12.2.0",
@@ -33,6 +35,48 @@
"lru-cache": "^10.4.3" "lru-cache": "^10.4.3"
} }
}, },
"node_modules/@blockly/field-colour": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/@blockly/field-colour/-/field-colour-6.0.11.tgz",
"integrity": "sha512-UUTwH3DMt1RslKWQIGAlN5cvGynoQ9mpWPKJGI4erG92wAQWKsLAh5ldVsv9TxO2EfNwpSmQ0/5JqTdSMMqdfA==",
"license": "Apache-2.0",
"dependencies": {
"@blockly/field-grid-dropdown": "^6.0.9"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"blockly": "^12.0.0"
}
},
"node_modules/@blockly/field-colour-hsv-sliders": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/@blockly/field-colour-hsv-sliders/-/field-colour-hsv-sliders-6.0.11.tgz",
"integrity": "sha512-l8sZezbdWIuIqqmhUacsuOlXDo/Un3YxbE7SSejx6Apb+5h0GAfQY9kxWVlHolan80CH2aLxFjkNZwiVkax6Kw==",
"license": "Apache-2.0",
"dependencies": {
"@blockly/field-colour": "^6.0.11"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"blockly": "^12.0.0"
}
},
"node_modules/@blockly/field-grid-dropdown": {
"version": "6.0.9",
"resolved": "https://registry.npmjs.org/@blockly/field-grid-dropdown/-/field-grid-dropdown-6.0.9.tgz",
"integrity": "sha512-YuPdS7ZhJKoVagPxbEwUPr5Gq14SnqNk6XD064mYGiH1OYO7LrtljESXG6cnnvQjhKkitiRjZ1ye20MzG3I6FQ==",
"license": "Apache 2.0",
"engines": {
"node": ">=8.17.0"
},
"peerDependencies": {
"blockly": "^12.0.0"
}
},
"node_modules/@blockly/plugin-strict-connection-checker": { "node_modules/@blockly/plugin-strict-connection-checker": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@blockly/plugin-strict-connection-checker/-/plugin-strict-connection-checker-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@blockly/plugin-strict-connection-checker/-/plugin-strict-connection-checker-6.0.1.tgz",

View File

@@ -12,6 +12,8 @@
"vite": "^7.0.4" "vite": "^7.0.4"
}, },
"dependencies": { "dependencies": {
"@blockly/field-colour": "^6.0.11",
"@blockly/field-colour-hsv-sliders": "^6.0.11",
"@blockly/plugin-strict-connection-checker": "^6.0.1", "@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1", "@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0", "blockly": "^12.2.0",

View File

@@ -117,7 +117,7 @@ Blockly.Blocks["every_seconds"] = {
init: function () { init: function () {
this.appendDummyInput() this.appendDummyInput()
.appendField("every") .appendField("every")
.appendField(new Blockly.FieldNumber(2, 0.1), "SECONDS") .appendField(new Blockly.FieldNumber(2, 0.01), "SECONDS")
.appendField("seconds"); .appendField("seconds");
this.appendStatementInput("DO").setCheck("default"); this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks"); this.setStyle("events_blocks");

View File

@@ -1,5 +1,6 @@
import * as Blockly from "blockly"; import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript"; import * as BlocklyJS from "blockly/javascript";
import {FieldColourHsvSliders} from '@blockly/field-colour-hsv-sliders';
function getAvailableCostumes() { function getAvailableCostumes() {
if (window.projectCostumes && window.projectCostumes.length > 0) { if (window.projectCostumes && window.projectCostumes.length > 0) {
@@ -216,3 +217,95 @@ BlocklyJS.javascriptGenerator.forBlock["switch_backdrop"] = function (block) {
const name = block.getFieldValue("BACKDROP_NAME"); const name = block.getFieldValue("BACKDROP_NAME");
return `setBackdropByName('${name}');\n`; return `setBackdropByName('${name}');\n`;
}; };
// Color/Tint effect block - with HSV color picker
Blockly.Blocks["set_color_effect"] = {
init: function () {
this.appendDummyInput()
.appendField("set color effect to")
.appendField(new FieldColourHsvSliders("#ff0000"), "COLOR");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_color_effect"] = function (block, generator) {
const color = block.getFieldValue("COLOR") || "#000000";
return `setSpriteEffect('tint', '${color}');\n`;
};
// Set effect by amount
Blockly.Blocks["set_effect"] = {
init: function () {
this.appendDummyInput()
.appendField("set")
.appendField(new Blockly.FieldDropdown([
["color", "color"],
["brightness", "brightness"],
["ghost", "ghost"],
["pixelate", "pixelate"],
["mosaic (WIP)", "mosaic"],
["whirl", "whirl"],
["fisheye", "fisheye"],
]), "EFFECT")
.appendField("effect to");
this.appendValueInput("VALUE")
.setCheck("Number");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_effect"] = function (block, generator) {
const effect = block.getFieldValue("EFFECT");
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC) || "0";
return `setSpriteEffect('${effect}', ${value});\n`;
};
// Change effect by amount
Blockly.Blocks["change_effect"] = {
init: function () {
this.appendDummyInput()
.appendField("change")
.appendField(new Blockly.FieldDropdown([
["color", "color"],
["brightness", "brightness"],
["ghost", "ghost"],
["pixelate", "pixelate"],
["mosaic (WIP)", "mosaic"],
["whirl", "whirl"],
["fisheye", "fisheye"],
]), "EFFECT")
.appendField("effect by");
this.appendValueInput("VALUE")
.setCheck("Number");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setInputsInline(true);
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["change_effect"] = function (block, generator) {
const effect = block.getFieldValue("EFFECT");
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC) || "0";
return `changeSpriteEffect('${effect}', ${value});\n`;
};
// Clear all effects
Blockly.Blocks["clear_effects"] = {
init: function () {
this.appendDummyInput().appendField("clear graphic effects");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["clear_effects"] = function () {
return `clearSpriteEffects();\n`;
};

90
src/blocks/monitors.js Normal file
View File

@@ -0,0 +1,90 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["show_variable"] = {
init: function () {
this.appendDummyInput()
.appendField("show variable")
.appendField(new Blockly.FieldDropdown(this.getVariables), "VAR");
this.appendDummyInput()
.appendField("at x:")
.appendField(new Blockly.FieldNumber(10), "X")
.appendField("y:")
.appendField(new Blockly.FieldNumber(10), "Y");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#FF8C1A");
this.setInputsInline(true);
},
getVariables: function() {
const variables = window.projectVariables || {};
const varNames = Object.keys(variables);
if (varNames.length === 0) {
return [["no variables", ""]];
}
return varNames.map(name => [name, name]);
}
};
BlocklyJS.javascriptGenerator.forBlock["show_variable"] = function (block) {
const varName = block.getFieldValue("VAR");
const x = block.getFieldValue("X") || 10;
const y = block.getFieldValue("Y") || 10;
return `showVariableMonitor("${varName}", ${x}, ${y});\n`;
};
Blockly.Blocks["hide_variable"] = {
init: function () {
this.appendDummyInput()
.appendField("hide variable")
.appendField(new Blockly.FieldDropdown(this.getVariables), "VAR");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#FF8C1A");
},
getVariables: function() {
const variables = window.projectVariables || {};
const varNames = Object.keys(variables);
if (varNames.length === 0) {
return [["no variables", ""]];
}
return varNames.map(name => [name, name]);
}
};
BlocklyJS.javascriptGenerator.forBlock["hide_variable"] = function (block) {
const varName = block.getFieldValue("VAR");
return `hideVariableMonitor("${varName}");\n`;
};
// Block to MOVE the variable monitor to a position
Blockly.Blocks["move_variable_to"] = {
init: function () {
this.appendDummyInput()
.appendField("move variable")
.appendField(new Blockly.FieldDropdown(this.getVariables), "VAR")
.appendField("to x:")
.appendField(new Blockly.FieldNumber(10), "X")
.appendField("y:")
.appendField(new Blockly.FieldNumber(10), "Y");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#FF8C1A");
},
getVariables: function() {
const variables = window.projectVariables || {};
const varNames = Object.keys(variables);
if (varNames.length === 0) {
return [["no variables", ""]];
}
return varNames.map(name => [name, name]);
}
};
BlocklyJS.javascriptGenerator.forBlock["move_variable_to"] = function (block) {
const varName = block.getFieldValue("VAR");
const x = block.getFieldValue("X") || 10;
const y = block.getFieldValue("Y") || 10;
console.log('Generating move_variable_to code:', varName, x, y);
return `moveVariableMonitor("${varName}", ${x}, ${y});\n`;
};

278
src/blocks/sensing.js Normal file
View File

@@ -0,0 +1,278 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
import {FieldColourHsvSliders} from '@blockly/field-colour-hsv-sliders';
// Function to get available sprites for dropdown
function getAvailableSprites() {
if (window.sprites && window.sprites.length > 0) {
return window.sprites.map(sprite => [sprite.id, sprite.id]);
}
return [["no sprites", ""]];
}
// Touching block
Blockly.Blocks["touching"] = {
init: function () {
this.appendDummyInput("MAIN")
.appendField("touching")
.appendField(
new Blockly.FieldDropdown([
["mouse pointer", "mouse"],
["edge", "edge"],
["sprite", "sprite"],
]),
"TARGET"
);
this.setOutput(true, "Boolean");
this.setColour("#4CBFE6");
this.setTooltip("Check if sprite is touching something");
},
onchange: function(e) {
if (e.type === Blockly.Events.BLOCK_FIELD_INTERMEDIATE_CHANGE) return;
if (!this.workspace) return;
const target = this.getFieldValue('TARGET');
const spriteDropdown = this.getField('SPRITE_ID');
const mainInput = this.getInput('MAIN');
if (target === 'sprite') {
if (!spriteDropdown) {
mainInput.appendField("named");
mainInput.appendField(new Blockly.FieldDropdown(
function() {
return getAvailableSprites();
}
), "SPRITE_ID");
}
} else {
if (spriteDropdown) {
// Remove the dropdown first
mainInput.removeField('SPRITE_ID');
// Find and remove the "named" label
const fields = mainInput.fieldRow;
for (let i = fields.length - 1; i >= 0; i--) {
const field = fields[i];
if (field.getText && field.getText() === "named") {
mainInput.removeField(fields[i].name, true);
break;
}
}
}
}
}
};
BlocklyJS.javascriptGenerator.forBlock["touching"] = function (block, generator) {
const target = block.getFieldValue("TARGET");
if (target === "mouse") {
return [`isTouchingMouse()`, BlocklyJS.Order.FUNCTION_CALL];
} else if (target === "edge") {
return [`isTouchingEdge()`, BlocklyJS.Order.FUNCTION_CALL];
} else if (target === "sprite") {
const spriteId = block.getFieldValue("SPRITE_ID") || "";
return [`isTouchingSprite("${spriteId}")`, BlocklyJS.Order.FUNCTION_CALL];
}
return ["false", BlocklyJS.Order.ATOMIC];
};
// Mouse down block
Blockly.Blocks["mouse_down"] = {
init: function () {
this.appendDummyInput().appendField("mouse down?");
this.setOutput(true, "Boolean");
this.setColour("#4CBFE6");
this.setTooltip("Check if mouse button is pressed");
},
};
BlocklyJS.javascriptGenerator.forBlock["mouse_down"] = function () {
return [`isMouseDown()`, BlocklyJS.Order.FUNCTION_CALL];
};
// Touching color block with full slider control
Blockly.Blocks["touching_color"] = {
init: function () {
this.appendDummyInput()
.appendField("touching color")
.appendField(new FieldColourHsvSliders("#ff0000"), "COLOR");
this.setOutput(true, "Boolean");
this.setColour("#4CBFE6");
this.setTooltip("Check if sprite is touching a specific color");
},
};
BlocklyJS.javascriptGenerator.forBlock["touching_color"] = function (block) {
const color = block.getFieldValue("COLOR");
return [`isTouchingColor("${color}")`, BlocklyJS.Order.FUNCTION_CALL];
};
// Distance to block - FIXED VERSION
Blockly.Blocks["distance_to"] = {
init: function () {
this.appendDummyInput("MAIN")
.appendField("distance to")
.appendField(
new Blockly.FieldDropdown([
["mouse pointer", "mouse"],
["sprite", "sprite"],
]),
"TARGET"
);
this.setOutput(true, "Number");
this.setColour("#4CBFE6");
this.setTooltip("Get distance to target");
},
onchange: function(e) {
if (e.type === Blockly.Events.BLOCK_FIELD_INTERMEDIATE_CHANGE) return;
if (!this.workspace) return;
const target = this.getFieldValue('TARGET');
const spriteDropdown = this.getField('SPRITE_ID');
const mainInput = this.getInput('MAIN');
if (target === 'sprite') {
if (!spriteDropdown) {
mainInput.appendField("named");
mainInput.appendField(new Blockly.FieldDropdown(
function() {
return getAvailableSprites();
}
), "SPRITE_ID");
}
} else {
if (spriteDropdown) {
// Remove the dropdown first
mainInput.removeField('SPRITE_ID');
// Find and remove the "named" label
const fields = mainInput.fieldRow;
for (let i = fields.length - 1; i >= 0; i--) {
const field = fields[i];
if (field.getText && field.getText() === "named") {
mainInput.removeField(fields[i].name, true);
break;
}
}
}
}
}
};
BlocklyJS.javascriptGenerator.forBlock["distance_to"] = function (block, generator) {
const target = block.getFieldValue("TARGET");
if (target === "mouse") {
return [`distanceToMouse()`, BlocklyJS.Order.FUNCTION_CALL];
} else if (target === "sprite") {
const spriteId = block.getFieldValue("SPRITE_ID") || "";
return [`distanceToSprite("${spriteId}")`, BlocklyJS.Order.FUNCTION_CALL];
}
return ["0", BlocklyJS.Order.ATOMIC];
};
// Answer (for ask and wait)
Blockly.Blocks["answer"] = {
init: function () {
this.appendDummyInput().appendField("answer");
this.setOutput(true, "String");
this.setColour("#4CBFE6");
this.setTooltip("The answer from the last 'ask and wait' prompt");
},
};
BlocklyJS.javascriptGenerator.forBlock["answer"] = function () {
return [`getAnswer()`, BlocklyJS.Order.FUNCTION_CALL];
};
// Ask and wait
Blockly.Blocks["ask_and_wait"] = {
init: function () {
this.appendValueInput("QUESTION")
.setCheck("String")
.appendField("ask")
.appendField("and wait");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setColour("#4CBFE6");
this.setTooltip("Ask a question and wait for user input");
},
};
BlocklyJS.javascriptGenerator.forBlock["ask_and_wait"] = function (block, generator) {
const question = generator.valueToCode(block, "QUESTION", BlocklyJS.Order.ATOMIC) || '"What\'s your name?"';
return `await askAndWait(${question});\n`;
};
// Username
Blockly.Blocks["username"] = {
init: function () {
this.appendDummyInput().appendField("username");
this.setOutput(true, "String");
this.setColour("#4CBFE6");
this.setTooltip("Get the current username");
},
};
BlocklyJS.javascriptGenerator.forBlock["username"] = function () {
return [`getUsername()`, BlocklyJS.Order.FUNCTION_CALL];
};
// Loudness
Blockly.Blocks["loudness"] = {
init: function () {
this.appendDummyInput().appendField("loudness");
this.setOutput(true, "Number");
this.setColour("#4CBFE6");
this.setTooltip("Microphone loudness (0-100)");
},
};
BlocklyJS.javascriptGenerator.forBlock["loudness"] = function () {
return [`getLoudness()`, BlocklyJS.Order.FUNCTION_CALL];
};
// Current date/time
Blockly.Blocks["current"] = {
init: function () {
this.appendDummyInput()
.appendField("current")
.appendField(
new Blockly.FieldDropdown([
["year", "year"],
["month", "month"],
["date", "date"],
["day of week", "dayofweek"],
["hour", "hour"],
["minute", "minute"],
["second", "second"],
]),
"UNIT"
);
this.setOutput(true, "Number");
this.setColour("#4CBFE6");
this.setTooltip("Get current date/time value");
},
};
BlocklyJS.javascriptGenerator.forBlock["current"] = function (block) {
const unit = block.getFieldValue("UNIT");
return [`getCurrent("${unit}")`, BlocklyJS.Order.FUNCTION_CALL];
};
// Days since 2000
Blockly.Blocks["days_since_2000"] = {
init: function () {
this.appendDummyInput().appendField("days since 2000");
this.setOutput(true, "Number");
this.setColour("#4CBFE6");
this.setTooltip("Number of days since January 1, 2000");
},
};
BlocklyJS.javascriptGenerator.forBlock["days_since_2000"] = function () {
return [`getDaysSince2000()`, BlocklyJS.Order.FUNCTION_CALL];
};

View File

@@ -0,0 +1,77 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
Blockly.Blocks["display_text_as_sprite"] = {
init: function () {
this.appendValueInput("TEXT")
.setCheck("String")
.appendField("display text");
this.appendDummyInput()
.appendField("font size")
.appendField(new Blockly.FieldNumber(32, 1, 500), "SIZE");
this.appendDummyInput()
.appendField("color")
.appendField(new Blockly.FieldTextInput("#ffffff"), "COLOR"); // CHANGED: Use FieldTextInput instead
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
this.setTooltip("Display text as the sprite");
},
};
BlocklyJS.javascriptGenerator.forBlock["display_text_as_sprite"] = function (
block,
generator
) {
const text = generator.valueToCode(block, "TEXT", BlocklyJS.Order.ATOMIC) || '""';
const size = block.getFieldValue("SIZE");
const color = block.getFieldValue("COLOR");
return `displayTextAsSprite(${text}, ${size}, "${color}");\n`;
};
Blockly.Blocks["clear_text_sprite"] = {
init: function () {
this.appendDummyInput().appendField("clear text sprite");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
this.setTooltip("Remove text and restore original sprite");
},
};
BlocklyJS.javascriptGenerator.forBlock["clear_text_sprite"] = function () {
return `clearTextSprite();\n`;
};
Blockly.Blocks["set_text_property"] = {
init: function () {
this.appendValueInput("VALUE")
.appendField("set text")
.appendField(
new Blockly.FieldDropdown([
["font", "font"],
["alignment", "align"],
["bold", "bold"],
["italic", "italic"],
["outline color", "stroke"],
["outline thickness", "strokeThickness"],
]),
"PROPERTY"
)
.appendField("to");
this.setPreviousStatement(true, "default");
this.setNextStatement(true, "default");
this.setStyle("looks_blocks");
},
};
BlocklyJS.javascriptGenerator.forBlock["set_text_property"] = function (
block,
generator
) {
const property = block.getFieldValue("PROPERTY");
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC) || '""';
return `setTextProperty("${property}", ${value});\n`;
};

172
src/functions/monitors.js Normal file
View File

@@ -0,0 +1,172 @@
import * as PIXI from "pixi.js-legacy";
const monitors = [];
const MONITOR_PADDING = 8;
const MONITOR_FONT_SIZE = 14;
const MONITOR_MIN_WIDTH = 80;
const MONITOR_BG_COLOR = 0xFF8C1A; // Orange for variables
const MONITOR_TEXT_COLOR = 0xFFFFFF;
export class Monitor {
constructor(app, type, name, getValue, x = 10, y = 10) {
this.app = app;
this.type = type; // 'variable', 'timer', 'answer', etc.
this.name = name;
this.label = name;
this.getValue = getValue;
this.visible = true;
this.dragging = false;
this.container = new PIXI.Container();
this.container.x = x;
this.container.y = y;
this.container.interactive = true;
this.container.buttonMode = true;
// Background
this.background = new PIXI.Graphics();
this.container.addChild(this.background);
// Label text
this.labelText = new PIXI.Text(name, {
fontFamily: 'Arial',
fontSize: MONITOR_FONT_SIZE,
fill: MONITOR_TEXT_COLOR,
fontWeight: 'bold'
});
this.labelText.x = MONITOR_PADDING;
this.labelText.y = MONITOR_PADDING;
this.container.addChild(this.labelText);
// Value text
this.valueText = new PIXI.Text('0', {
fontFamily: 'Arial',
fontSize: MONITOR_FONT_SIZE + 2,
fill: MONITOR_TEXT_COLOR
});
this.valueText.x = MONITOR_PADDING;
this.valueText.y = MONITOR_PADDING + MONITOR_FONT_SIZE + 4;
this.container.addChild(this.valueText);
// Make draggable
this.container.on('pointerdown', this.onDragStart.bind(this));
this.container.on('pointerup', this.onDragEnd.bind(this));
this.container.on('pointerupoutside', this.onDragEnd.bind(this));
this.container.on('pointermove', this.onDragMove.bind(this));
this.app.stage.addChild(this.container);
this.updateDisplay();
monitors.push(this);
}
onDragStart(event) {
this.dragging = true;
this.dragData = event.data;
const pos = this.dragData.getLocalPosition(this.app.stage);
this.dragOffset = {
x: pos.x - this.container.x,
y: pos.y - this.container.y
};
}
onDragEnd() {
this.dragging = false;
this.dragData = null;
}
onDragMove() {
if (this.dragging && this.dragData) {
const pos = this.dragData.getLocalPosition(this.app.stage);
this.container.x = pos.x - this.dragOffset.x;
this.container.y = pos.y - this.dragOffset.y;
}
}
updateDisplay() {
const value = this.getValue();
this.valueText.text = String(value);
// Resize background
const width = Math.max(
MONITOR_MIN_WIDTH,
Math.max(this.labelText.width, this.valueText.width) + MONITOR_PADDING * 2
);
const height = this.labelText.height + this.valueText.height + MONITOR_PADDING * 3;
this.background.clear();
this.background.beginFill(MONITOR_BG_COLOR);
this.background.drawRoundedRect(0, 0, width, height, 8);
this.background.endFill();
this.background.lineStyle(2, 0x000000, 0.2);
this.background.drawRoundedRect(0, 0, width, height, 8);
}
setVisible(visible) {
this.visible = visible;
this.container.visible = visible;
}
destroy() {
this.container.destroy({ children: true });
const index = monitors.indexOf(this);
if (index > -1) {
monitors.splice(index, 1);
}
}
toJSON() {
return {
type: this.type,
name: this.name,
x: this.container.x,
y: this.container.y,
visible: this.visible
};
}
}
export function updateAllMonitors() {
monitors.forEach(monitor => monitor.updateDisplay());
}
export function getMonitor(type, name) {
return monitors.find(m => m.type === type && m.name === name);
}
export function createMonitor(app, type, name, getValue, x, y) {
// Check if monitor already exists
const existing = getMonitor(type, name);
if (existing) {
existing.setVisible(true);
return existing;
}
return new Monitor(app, type, name, getValue, x, y);
}
export function removeMonitor(type, name) {
const monitor = getMonitor(type, name);
if (monitor) {
monitor.destroy();
}
}
export function getAllMonitors() {
return monitors;
}
export function clearAllMonitors() {
monitors.forEach(m => m.destroy());
monitors.length = 0;
}
export function loadMonitors(app, monitorsData, valueGetters) {
monitorsData.forEach(data => {
const getter = valueGetters[data.type]?.[data.name];
if (getter) {
const monitor = new Monitor(app, data.type, data.name, getter, data.x, data.y);
monitor.setVisible(data.visible);
}
});
}

View File

@@ -437,5 +437,741 @@ export function runCodeWithFunctions({
if (spriteData.currentBubble) spriteData.currentBubble.visible = bool; if (spriteData.currentBubble) spriteData.currentBubble.visible = bool;
} }
function displayTextAsSprite(text, fontSize, color) {
// Store original texture if not already stored
if (!spriteData.originalTexture) {
spriteData.originalTexture = sprite.texture;
spriteData.originalAnchor = { x: sprite.anchor.x, y: sprite.anchor.y };
}
// Remove old text if it exists
if (spriteData.textSprite) {
sprite.removeChild(spriteData.textSprite);
spriteData.textSprite.destroy();
spriteData.textSprite = null;
}
// Create or update text style
if (!spriteData.textStyle) {
spriteData.textStyle = {
fontFamily: 'Arial',
fontSize: fontSize || 32,
fill: color || '#ffffff',
align: 'center',
fontWeight: 'normal',
fontStyle: 'normal',
stroke: '#000000',
strokeThickness: 0,
};
} else {
spriteData.textStyle.fontSize = fontSize || spriteData.textStyle.fontSize;
spriteData.textStyle.fill = color || spriteData.textStyle.fill;
}
// Create PIXI Text
const pixiText = new PIXI.Text(String(text), spriteData.textStyle);
pixiText.anchor.set(0.5);
// Hide the original sprite texture
sprite.texture = PIXI.Texture.EMPTY;
// Add text as child
sprite.addChild(pixiText);
spriteData.textSprite = pixiText;
}
function clearTextSprite() {
// Remove text sprite
if (spriteData.textSprite) {
sprite.removeChild(spriteData.textSprite);
spriteData.textSprite.destroy();
spriteData.textSprite = null;
}
// Restore original texture
if (spriteData.originalTexture) {
sprite.texture = spriteData.originalTexture;
sprite.anchor.set(spriteData.originalAnchor.x, spriteData.originalAnchor.y);
}
// Clear stored style
spriteData.textStyle = null;
}
function setTextProperty(property, value) {
if (!spriteData.textStyle) {
spriteData.textStyle = {
fontFamily: 'Arial',
fontSize: 32,
fill: '#ffffff',
align: 'center',
fontWeight: 'normal',
fontStyle: 'normal',
stroke: '#000000',
strokeThickness: 0,
};
}
switch (property) {
case 'font':
spriteData.textStyle.fontFamily = String(value);
break;
case 'align':
spriteData.textStyle.align = String(value);
break;
case 'bold':
spriteData.textStyle.fontWeight = value ? 'bold' : 'normal';
break;
case 'italic':
spriteData.textStyle.fontStyle = value ? 'italic' : 'normal';
break;
case 'stroke':
spriteData.textStyle.stroke = String(value);
break;
case 'strokeThickness':
spriteData.textStyle.strokeThickness = Number(value) || 0;
break;
}
// Update existing text sprite if it exists
if (spriteData.textSprite) {
spriteData.textSprite.style = new PIXI.TextStyle(spriteData.textStyle);
}
}
// Touching mouse pointer
function isTouchingMouse() {
const mouse = renderer.events.pointer.global;
const bounds = sprite.getBounds();
return bounds.contains(mouse.x, mouse.y);
}
// Touching edge
function isTouchingEdge() {
const bounds = sprite.getBounds();
const stageWidth = renderer.width;
const stageHeight = renderer.height;
return (
bounds.left <= 0 ||
bounds.right >= stageWidth ||
bounds.top <= 0 ||
bounds.bottom >= stageHeight
);
}
// Touching another sprite
function isTouchingSprite(spriteName) {
const targetSpriteData = window.sprites?.find(s => s.id === spriteName);
if (!targetSpriteData) return false;
const targetSprite = targetSpriteData.pixiSprite;
const bounds1 = sprite.getBounds();
const bounds2 = targetSprite.getBounds();
return bounds1.intersects(bounds2);
}
// Mouse down
function isMouseDown() {
return Object.values(mouseButtonsPressed).some((pressed) => pressed);
}
// Touching color (simplified - checks if sprite overlaps with any pixel of that color on stage)
// Touching color (improved version)
function isTouchingColor(hexColor) {
// Convert hex to RGB
const color = parseInt(hexColor.replace('#', ''), 16);
const targetR = (color >> 16) & 0xFF;
const targetG = (color >> 8) & 0xFF;
const targetB = color & 0xFF;
// Get sprite bounds in screen coordinates
const bounds = sprite.getBounds();
// Sample points around the sprite (not just inside)
const samplePoints = [];
const step = 5; // Sample every 5 pixels
for (let x = bounds.left; x < bounds.right; x += step) {
for (let y = bounds.top; y < bounds.bottom; y += step) {
samplePoints.push({ x: Math.floor(x), y: Math.floor(y) });
}
}
// Add edge points
for (let x = bounds.left; x < bounds.right; x += step) {
samplePoints.push({ x: Math.floor(x), y: Math.floor(bounds.top) });
samplePoints.push({ x: Math.floor(x), y: Math.floor(bounds.bottom) });
}
for (let y = bounds.top; y < bounds.bottom; y += step) {
samplePoints.push({ x: Math.floor(bounds.left), y: Math.floor(y) });
samplePoints.push({ x: Math.floor(bounds.right), y: Math.floor(y) });
}
// Extract pixels from the entire stage
try {
const renderTexture = PIXI.RenderTexture.create({
width: renderer.width,
height: renderer.height
});
renderer.render(stage, { renderTexture });
const pixels = renderer.extract.pixels(renderTexture);
const width = renderer.width;
// Check sampled points
for (const point of samplePoints) {
const x = point.x;
const y = point.y;
if (x < 0 || x >= width || y < 0 || y >= renderer.height) continue;
const index = (y * width + x) * 4;
const pixelR = pixels[index];
const pixelG = pixels[index + 1];
const pixelB = pixels[index + 2];
const pixelA = pixels[index + 3];
// Skip transparent pixels
if (pixelA < 128) continue;
// Check if color matches (with tolerance of 20)
const tolerance = 20;
if (Math.abs(pixelR - targetR) <= tolerance &&
Math.abs(pixelG - targetG) <= tolerance &&
Math.abs(pixelB - targetB) <= tolerance) {
renderTexture.destroy();
return true;
}
}
renderTexture.destroy();
return false;
} catch (err) {
console.error("Error checking color:", err);
return false;
}
}
// Distance to mouse
function distanceToMouse() {
const mouse = renderer.events.pointer.global;
const spriteX = sprite.x + renderer.width / 2;
const spriteY = sprite.y + renderer.height / 2;
const dx = mouse.x - spriteX;
const dy = mouse.y - spriteY;
return Math.sqrt(dx * dx + dy * dy) / stage.scale.x;
}
// Distance to sprite
function distanceToSprite(spriteName) {
const targetSpriteData = window.sprites?.find(s => s.id === spriteName);
if (!targetSpriteData) return 0;
const targetSprite = targetSpriteData.pixiSprite;
const dx = targetSprite.x - sprite.x;
const dy = targetSprite.y - sprite.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Ask and wait
let lastAnswer = "";
async function askAndWait(question) {
lastAnswer = prompt(String(question)) || "";
return lastAnswer;
}
function getAnswer() {
return lastAnswer;
}
// Username
function getUsername() {
// Try to get from localStorage or return "user"
return localStorage.getItem("username") || "user";
}
// Loudness (placeholder - would need microphone access)
function getLoudness() {
return 0; // Would need Web Audio API implementation
}
// Current date/time
function getCurrent(unit) {
const now = new Date();
switch (unit) {
case "year":
return now.getFullYear();
case "month":
return now.getMonth() + 1; // 1-12
case "date":
return now.getDate();
case "dayofweek":
return now.getDay() + 1; // 1-7 (1 = Sunday)
case "hour":
return now.getHours();
case "minute":
return now.getMinutes();
case "second":
return now.getSeconds();
default:
return 0;
}
}
// Days since 2000
function getDaysSince2000() {
const now = new Date();
const year2000 = new Date(2000, 0, 1);
const diff = now.getTime() - year2000.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
function showVariableMonitor(varName, x, y) {
console.log('=== showVariableMonitor called ===');
console.log('Variable:', varName, 'Position:', x, y);
const existing = window.getMonitor?.('variable', varName);
if (existing) {
console.log('Monitor already exists, moving to:', x, y);
existing.setVisible(true);
// FLIP Y coordinate: negate it to match stage coordinates
existing.container.x = x !== undefined ? x : 10;
existing.container.y = y !== undefined ? -y : -10;
} else {
console.log('Creating new monitor at:', x, y);
const newMonitor = window.createMonitor?.(
window.app,
'variable',
varName,
() => window.projectVariables[varName],
x !== undefined ? x : 10,
y !== undefined ? -y : -10 // FLIP Y here too
);
console.log('Monitor created:', !!newMonitor);
}
}
function hideVariableMonitor(varName) {
window.removeMonitor?.('variable', varName);
}
function gotoVariableMonitor(varName) {
console.log('=== gotoVariableMonitor called ===');
console.log('Looking for variable:', varName);
const monitors = window.getAllMonitors?.() || [];
const monitor = monitors.find(m => m.type === 'variable' && m.name === varName);
if (monitor) {
console.log('✓ Monitor found!');
console.log('Monitor container.x:', monitor.container.x);
console.log('Monitor container.y:', monitor.container.y);
console.log('Stage position:', { x: stage.x, y: stage.y });
console.log('Stage scale:', { x: stage.scale.x, y: stage.scale.y });
console.log('Renderer size:', { width: renderer.width, height: renderer.height });
console.log('App stage size:', { width: app.stageWidth, height: app.stageHeight });
console.log('Current sprite position:', { x: sprite.x, y: sprite.y });
// The monitor is a direct child of app.stage
// Sprites use coordinates where (0,0) is center
// So we can directly use the monitor's position
sprite.x = monitor.container.x;
sprite.y = monitor.container.y;
console.log('New sprite position:', { x: sprite.x, y: sprite.y });
} else {
console.log('✗ Monitor NOT found');
}
}
function moveVariableMonitor(varName, x, y) {
console.log('=== moveVariableMonitor called ===');
console.log('Variable:', varName, 'New position:', x, y);
setTimeout(() => {
const monitor = window.getMonitor?.('variable', varName);
console.log('Monitor found (after timeout):', !!monitor);
if (monitor && monitor.container) {
console.log('Current position:', monitor.container.x, monitor.container.y);
monitor.container.x = x;
monitor.container.y = -y; // FLIP Y coordinate
console.log('New position set to:', monitor.container.x, monitor.container.y);
} else {
console.log('Monitor or container not found!');
}
}, 0);
}
// Sprite effects
function setSpriteEffect(effect, value) {
if (!spriteData.effects) {
spriteData.effects = {
color: 0,
brightness: 0,
ghost: 0,
pixelate: 0,
mosaic: 0,
whirl: 0,
fisheye: 0,
tint: null
};
}
if (effect === 'tint') {
// For color effect with hex color picker
spriteData.effects.tint = value;
if (value && typeof value === 'string') {
const color = parseInt(value.replace('#', ''), 16);
sprite.tint = color;
} else {
sprite.tint = 0xFFFFFF;
}
} else {
spriteData.effects[effect] = Number(value) || 0;
applySpriteEffects();
}
}
function changeSpriteEffect(effect, value) {
if (!spriteData.effects) {
spriteData.effects = {
color: 0,
brightness: 0,
ghost: 0,
pixelate: 0,
mosaic: 0,
whirl: 0,
fisheye: 0,
tint: null
};
}
spriteData.effects[effect] = (spriteData.effects[effect] || 0) + (Number(value) || 0);
applySpriteEffects();
}
function clearSpriteEffects() {
spriteData.effects = {
color: 0,
brightness: 0,
ghost: 0,
pixelate: 0,
mosaic: 0,
whirl: 0,
fisheye: 0,
tint: null
};
sprite.tint = 0xFFFFFF;
sprite.alpha = 1;
sprite.filters = null;
applySpriteEffects();
}
function applySpriteEffects() {
if (!spriteData.effects) return;
const effects = spriteData.effects;
const filters = [];
// Ghost effect (transparency) - 0 to 100
sprite.alpha = Math.max(0, Math.min(1, 1 - (effects.ghost / 100)));
// Color effect (hue shift) - 0 to 200
if (effects.color !== 0 && !effects.tint) {
const hue = (effects.color % 200) / 200; // 0 to 1
const rgb = hsvToRgb(hue, 1, 1);
sprite.tint = (rgb.r << 16) | (rgb.g << 8) | rgb.b;
} else if (!effects.tint) {
sprite.tint = 0xFFFFFF;
}
// Brightness effect using ColorMatrixFilter
if (effects.brightness !== 0) {
const brightnessFilter = new PIXI.filters.ColorMatrixFilter();
brightnessFilter.brightness(1 + (effects.brightness / 100), false);
filters.push(brightnessFilter);
}
// Pixelate effect with custom shader
if (effects.pixelate !== 0) {
const pixelSize = Math.max(1, Math.abs(effects.pixelate / 10));
const pixelateFilter = createPixelateFilter(pixelSize);
filters.push(pixelateFilter);
}
// Mosaic effect (creates a tiled/blocky pattern)
if (effects.mosaic !== 0) {
const mosaicSize = Math.max(1, Math.abs(effects.mosaic / 3));
const mosaicFilter = createMosaicFilter(mosaicSize);
filters.push(mosaicFilter);
}
// Whirl/Twist effect
if (effects.whirl !== 0) {
const whirlFilter = createWhirlFilter(effects.whirl);
filters.push(whirlFilter);
}
// Fisheye effect
if (effects.fisheye !== 0) {
const fisheyeFilter = createFisheyeFilter(effects.fisheye);
filters.push(fisheyeFilter);
}
// Apply all filters
sprite.filters = filters.length > 0 ? filters : null;
}
// Create pixelate filter with custom shader
function createPixelateFilter(pixelSize) {
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform vec2 dimensions;
uniform float pixelSize;
void main() {
vec2 coord = vTextureCoord * dimensions;
vec2 pixelated = floor(coord / pixelSize) * pixelSize;
vec2 uv = pixelated / dimensions;
gl_FragColor = texture2D(uSampler, uv);
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
dimensions: [sprite.texture.width, sprite.texture.height],
pixelSize: pixelSize
});
return filter;
}
// Create whirl/twist filter with custom shader
function createWhirlFilter(amount) {
const radius = 0.5;
const angle = amount * 0.05;
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float angle;
uniform float radius;
void main() {
vec2 coord = vTextureCoord - 0.5;
float dist = length(coord);
if (dist < radius) {
float percent = (radius - dist) / radius;
float theta = percent * percent * angle;
float s = sin(theta);
float c = cos(theta);
coord = vec2(
coord.x * c - coord.y * s,
coord.x * s + coord.y * c
);
}
coord += 0.5;
gl_FragColor = texture2D(uSampler, coord);
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
angle: angle,
radius: radius
});
return filter;
}
// Create fisheye filter with custom shader
function createFisheyeFilter(amount) {
const strength = amount / 100;
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float strength;
void main() {
vec2 coord = vTextureCoord - 0.5;
float dist = length(coord);
if (dist > 0.0) {
float distortion = 1.0 + dist * strength;
coord = coord / distortion;
}
coord += 0.5;
if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) {
gl_FragColor = vec4(0.0);
} else {
gl_FragColor = texture2D(uSampler, coord);
}
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
strength: strength
});
return filter;
}
// Create mosaic filter - creates a repeating/mirrored tile pattern
function createMosaicFilter(size) {
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform vec2 dimensions;
uniform float size;
void main() {
vec2 coord = vTextureCoord * dimensions;
// Create repeating tiles by using modulo
vec2 tileCoord = mod(coord, size * 2.0);
// Mirror alternating tiles for a kaleidoscope effect
vec2 tile = floor(coord / (size * 2.0));
if (mod(tile.x, 2.0) > 0.5) {
tileCoord.x = size * 2.0 - tileCoord.x;
}
if (mod(tile.y, 2.0) > 0.5) {
tileCoord.y = size * 2.0 - tileCoord.y;
}
vec2 uv = tileCoord / dimensions;
gl_FragColor = texture2D(uSampler, uv);
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
dimensions: [sprite.texture.width, sprite.texture.height],
size: size
});
return filter;
}
// Helper function to convert HSV to RGB
function hsvToRgb(h, s, v) {
let r, g, b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
// Create whirl/twist filter with custom shader
function createWhirlFilter(amount) {
const radius = 0.5;
const angle = amount * 0.05; // Scale down the effect
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float angle;
uniform float radius;
void main() {
vec2 coord = vTextureCoord - 0.5;
float dist = length(coord);
if (dist < radius) {
float percent = (radius - dist) / radius;
float theta = percent * percent * angle;
float s = sin(theta);
float c = cos(theta);
coord = vec2(
coord.x * c - coord.y * s,
coord.x * s + coord.y * c
);
}
coord += 0.5;
gl_FragColor = texture2D(uSampler, coord);
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
angle: angle,
radius: radius
});
return filter;
}
// Create fisheye filter with custom shader
function createFisheyeFilter(amount) {
const strength = amount / 100;
const fragmentShader = `
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float strength;
void main() {
vec2 coord = vTextureCoord - 0.5;
float dist = length(coord);
if (dist > 0.0) {
float distortion = 1.0 + dist * strength;
coord = coord / distortion;
}
coord += 0.5;
if (coord.x < 0.0 || coord.x > 1.0 || coord.y < 0.0 || coord.y > 1.0) {
gl_FragColor = vec4(0.0);
} else {
gl_FragColor = texture2D(uSampler, coord);
}
}
`;
const filter = new PIXI.Filter(null, fragmentShader, {
strength: strength
});
return filter;
}
eval(code); eval(code);
} }

View File

@@ -24,11 +24,20 @@ import {
} from "../functions/extensionManager.js"; } from "../functions/extensionManager.js";
import { Thread } from "../functions/threads.js"; import { Thread } from "../functions/threads.js";
import { runCodeWithFunctions } from "../functions/runCode.js"; import { runCodeWithFunctions } from "../functions/runCode.js";
import {
createMonitor,
removeMonitor,
updateAllMonitors,
getAllMonitors,
clearAllMonitors,
loadMonitors,
getMonitor
} from "../functions/monitors.js";
import config from "../config"; import config from "../config";
BlocklyJS.javascriptGenerator.addReservedWords( BlocklyJS.javascriptGenerator.addReservedWords(
"whenFlagClicked,moveSteps,getAngle,getMousePosition,sayMessage,waitOneFrame,wait,switchCostume,setSize,setAngle,projectTime,isKeyPressed,isMouseButtonPressed,getCostumeSize,getSpriteScale,_startTween,startTween,soundProperties,setSoundProperty,playSound,stopSound,stopAllSounds,isMouseTouchingSprite,setPenStatus,setPenColor,setPenColorHex,setPenSize,clearPen,Thread,fastExecution,BUBBLE_TEXTSTYLE,sprite,renderer,stage,costumeMap,soundMap,stopped,code,penGraphics,runningScripts,findOrFilterItem,registerEvent,triggerCustomEvent,hideSprite,showSprite,MyFunctions" "whenFlagClicked,moveSteps,getAngle,getMousePosition,sayMessage,waitOneFrame,wait,switchCostume,setSize,setAngle,projectTime,isKeyPressed,isMouseButtonPressed,getCostumeSize,getSpriteScale,_startTween,startTween,soundProperties,setSoundProperty,playSound,stopSound,stopAllSounds,isMouseTouchingSprite,setPenStatus,setPenColor,setPenColorHex,setPenSize,clearPen,Thread,fastExecution,BUBBLE_TEXTSTYLE,sprite,renderer,stage,costumeMap,soundMap,stopped,code,penGraphics,runningScripts,findOrFilterItem,registerEvent,triggerCustomEvent,hideSprite,showSprite,MyFunctions,displayTextAsSprite,clearTextSprite,setTextProperty,isTouchingMouse,isTouchingEdge,isTouchingSprite,isMouseDown,isTouchingColor,distanceToMouse,distanceToSprite,askAndWait,getAnswer,getUsername,getLoudness,getCurrent,getDaysSince2000,showVariableMonitor,hideVariableMonitor,gotoVariableMonitor,moveVariableMonitor,setSpriteEffect,changeSpriteEffect,clearSpriteEffects"
); );
import.meta.glob("../blocks/**/*.js", { eager: true }); import.meta.glob("../blocks/**/*.js", { eager: true });
@@ -40,6 +49,9 @@ let currentRoom = null;
let amHost = false; let amHost = false;
let invitesEnabled = true; let invitesEnabled = true;
let connectedUsers = []; let connectedUsers = [];
let mouseX = 0;
let mouseY = 0;
let mouseCoordsFrozen = false;
const wrapper = document.getElementById("stage-wrapper"); const wrapper = document.getElementById("stage-wrapper");
const stageContainer = document.getElementById("stage"); const stageContainer = document.getElementById("stage");
@@ -94,8 +106,10 @@ function createPenGraphics() {
} }
createPenGraphics(); createPenGraphics();
export let projectVariables = {}; window.projectVariables = {};
export let sprites = []; export const projectVariables = window.projectVariables;
window.sprites = [];
export const sprites = window.sprites;
export let activeSprite = null; export let activeSprite = null;
window.projectSounds = []; window.projectSounds = [];
window.projectCostumes = ["default"]; window.projectCostumes = ["default"];
@@ -186,6 +200,13 @@ workspace.registerToolboxCategoryCallback("GLOBAL_VARIABLES", function (_) {
varField.setAttribute("name", "VAR"); varField.setAttribute("name", "VAR");
varField.textContent = name; varField.textContent = name;
get.appendChild(varField); get.appendChild(varField);
// ADD THIS: Add checkbox for showing monitor
const checkbox = Blockly.utils.xml.createElement("field");
checkbox.setAttribute("name", "CHECKBOX");
checkbox.textContent = "FALSE";
get.appendChild(checkbox);
xmlList.push(get); xmlList.push(get);
} }
@@ -427,7 +448,10 @@ function renderSpriteInfo() {
const infoEl = document.getElementById("sprite-info"); const infoEl = document.getElementById("sprite-info");
if (!activeSprite) { if (!activeSprite) {
infoEl.innerHTML = "<p>Select a sprite to see its info.</p>"; infoEl.innerHTML = `
<p>Select a sprite to see its info.</p>
<p>Mouse: ${mouseX}, ${mouseY} ${mouseCoordsFrozen ? '🔒' : ''}</p>
`;
} else { } else {
const sprite = activeSprite.pixiSprite; const sprite = activeSprite.pixiSprite;
@@ -436,6 +460,7 @@ function renderSpriteInfo() {
<p>${Math.round(sprite.angle)}º</p> <p>${Math.round(sprite.angle)}º</p>
<p>size: ${Math.round(((sprite.scale.x + sprite.scale.y) / 2) * 100)}</p> <p>size: ${Math.round(((sprite.scale.x + sprite.scale.y) / 2) * 100)}</p>
<p><i class="fa-solid fa-${sprite.visible ? "eye" : "eye-slash"}"></i></p> <p><i class="fa-solid fa-${sprite.visible ? "eye" : "eye-slash"}"></i></p>
<p>Mouse: ${mouseX}, ${mouseY} ${mouseCoordsFrozen ? '🔒' : ''}</p>
`; `;
} }
} }
@@ -1024,8 +1049,21 @@ function stopAllScripts() {
clearTimeout(spriteData.sayTimeout); clearTimeout(spriteData.sayTimeout);
spriteData.sayTimeout = null; spriteData.sayTimeout = null;
} }
if (spriteData.textSprite) {
const sprite = spriteData.pixiSprite;
sprite.removeChild(spriteData.textSprite);
spriteData.textSprite.destroy();
spriteData.textSprite = null;
// Restore original texture
if (spriteData.originalTexture) {
sprite.texture = spriteData.originalTexture;
sprite.anchor.set(spriteData.originalAnchor.x, spriteData.originalAnchor.y);
} }
} }
}
updateAllMonitors();
}
async function runCode() { async function runCode() {
stopAllScripts(); stopAllScripts();
@@ -1103,6 +1141,11 @@ async function runCode() {
} }
} }
// Add ticker for updating monitors
app.ticker.add(() => {
updateAllMonitors();
});
app.view.addEventListener("click", () => { app.view.addEventListener("click", () => {
for (const entry of eventRegistry.stageClick) { for (const entry of eventRegistry.stageClick) {
entry.cb(); entry.cb();
@@ -1211,6 +1254,7 @@ export async function getProject() {
backdrops: backdropsData, backdrops: backdropsData,
currentBackdrop: currentBackdrop, currentBackdrop: currentBackdrop,
projectName: projectName, projectName: projectName,
monitors: getAllMonitors().map(m => m.toJSON()),
}; };
} }
@@ -1223,6 +1267,7 @@ async function saveProject() {
backdrops: [], // ADD THIS backdrops: [], // ADD THIS
currentBackdrop: currentBackdrop, // ADD THIS currentBackdrop: currentBackdrop, // ADD THIS
projectName: projectName, projectName: projectName,
monitors: getAllMonitors().map(m => m.toJSON()),
}; };
const toUint8Array = base64 => const toUint8Array = base64 =>
Uint8Array.from(atob(base64), c => c.charCodeAt(0)); Uint8Array.from(atob(base64), c => c.charCodeAt(0));
@@ -1516,14 +1561,20 @@ async function handleProjectData(data) {
for (const child of app.stage.removeChildren()) { for (const child of app.stage.removeChildren()) {
if (child.destroy) child.destroy({ children: true }); if (child.destroy) child.destroy({ children: true });
} }
sprites = []; sprites.length = 0;
window.sprites = sprites;
if (!Array.isArray(data.sprites)) { if (!Array.isArray(data.sprites)) {
window.alert("No valid sprites found in file."); window.alert("No valid sprites found in file.");
return; return;
} }
if (data.variables) projectVariables = data.variables; if (data.variables) {
for (const key in projectVariables) {
delete projectVariables[key];
}
Object.assign(projectVariables, data.variables);
}
// Reset arrays // Reset arrays
window.projectCostumes = ["default"]; window.projectCostumes = ["default"];
@@ -1678,6 +1729,27 @@ async function handleProjectData(data) {
// IMPORTANT: Update toolbox AFTER everything is loaded // IMPORTANT: Update toolbox AFTER everything is loaded
workspace.updateToolbox(document.getElementById('toolbox')); workspace.updateToolbox(document.getElementById('toolbox'));
if (Array.isArray(data.monitors)) {
clearAllMonitors();
const valueGetters = {
variable: {},
timer: {
'timer': () => projectTime()
},
answer: {
'answer': () => window.lastAnswer || ''
}
};
// Add getters for all variables
Object.keys(projectVariables).forEach(varName => {
valueGetters.variable[varName] = () => projectVariables[varName];
});
loadMonitors(app, data.monitors, valueGetters);
}
// Force refresh all blocks with dropdowns to show correct values // Force refresh all blocks with dropdowns to show correct values
setTimeout(() => { setTimeout(() => {
workspace.getAllBlocks(false).forEach(block => { workspace.getAllBlocks(false).forEach(block => {
@@ -1876,6 +1948,29 @@ window.addEventListener("resize", () => {
resizeCanvas(); resizeCanvas();
}); });
// Track mouse position relative to stage
app.view.addEventListener("mousemove", (e) => {
if (mouseCoordsFrozen) return;
const rect = app.view.getBoundingClientRect();
const mouseScreenX = e.clientX - rect.left;
const mouseScreenY = e.clientY - rect.top;
// Convert to stage coordinates
mouseX = Math.round((mouseScreenX - app.stage.x) / app.stage.scale.x);
mouseY = -Math.round((mouseScreenY - app.stage.y) / app.stage.scale.y);
renderSpriteInfo();
});
// Toggle freeze on spacebar
window.addEventListener("keydown", (e) => {
if (e.key === "h" && !e.repeat) {
mouseCoordsFrozen = !mouseCoordsFrozen;
renderSpriteInfo();
}
});
function isXmlEmpty(input = "") { function isXmlEmpty(input = "") {
input = input.trim(); input = input.trim();
return ( return (
@@ -2741,3 +2836,16 @@ workspace.addChangeListener(event => {
workspace.updateAllFunctionCalls = () => { workspace.updateAllFunctionCalls = () => {
updateAllFunctionCalls(workspace); updateAllFunctionCalls(workspace);
}; };
setInterval(() => {
window.projectVariables = projectVariables;
window.sprites = sprites;
}, 100);
window.app = app;
window.projectVariables = projectVariables;
window.sprites = sprites;
window.createMonitor = createMonitor;
window.removeMonitor = removeMonitor;
window.getAllMonitors = getAllMonitors;
window.getMonitor = getMonitor;