@@ -442,6 +482,28 @@
+
+
+
+
+
+
+
+
+
+
+ What's your name?
+
+
+
+
+
+
+
+
+
+
+
@@ -554,6 +616,12 @@
+
+
+
+
+
+
diff --git a/ext.js b/ext.js
deleted file mode 100644
index aacaabc..0000000
--- a/ext.js
+++ /dev/null
@@ -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);
diff --git a/package-lock.json b/package-lock.json
index 76fa927..b8e0ac5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "rarry-vite",
"version": "0.0.0",
"dependencies": {
+ "@blockly/field-colour": "^6.0.11",
+ "@blockly/field-colour-hsv-sliders": "^6.0.11",
"@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0",
@@ -33,6 +35,48 @@
"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": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@blockly/plugin-strict-connection-checker/-/plugin-strict-connection-checker-6.0.1.tgz",
diff --git a/package.json b/package.json
index 4b74d14..9a77f97 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
"vite": "^7.0.4"
},
"dependencies": {
+ "@blockly/field-colour": "^6.0.11",
+ "@blockly/field-colour-hsv-sliders": "^6.0.11",
"@blockly/plugin-strict-connection-checker": "^6.0.1",
"@fortawesome/fontawesome-free": "^7.0.1",
"blockly": "^12.2.0",
diff --git a/src/blocks/event.js b/src/blocks/event.js
index 55638a5..493e3ab 100644
--- a/src/blocks/event.js
+++ b/src/blocks/event.js
@@ -117,7 +117,7 @@ Blockly.Blocks["every_seconds"] = {
init: function () {
this.appendDummyInput()
.appendField("every")
- .appendField(new Blockly.FieldNumber(2, 0.1), "SECONDS")
+ .appendField(new Blockly.FieldNumber(2, 0.01), "SECONDS")
.appendField("seconds");
this.appendStatementInput("DO").setCheck("default");
this.setStyle("events_blocks");
diff --git a/src/blocks/looks.js b/src/blocks/looks.js
index 58183be..df7e684 100644
--- a/src/blocks/looks.js
+++ b/src/blocks/looks.js
@@ -1,5 +1,6 @@
import * as Blockly from "blockly";
import * as BlocklyJS from "blockly/javascript";
+import {FieldColourHsvSliders} from '@blockly/field-colour-hsv-sliders';
function getAvailableCostumes() {
if (window.projectCostumes && window.projectCostumes.length > 0) {
@@ -216,3 +217,95 @@ BlocklyJS.javascriptGenerator.forBlock["switch_backdrop"] = function (block) {
const name = block.getFieldValue("BACKDROP_NAME");
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`;
+};
\ No newline at end of file
diff --git a/src/blocks/monitors.js b/src/blocks/monitors.js
new file mode 100644
index 0000000..a701a78
--- /dev/null
+++ b/src/blocks/monitors.js
@@ -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`;
+};
\ No newline at end of file
diff --git a/src/blocks/sensing.js b/src/blocks/sensing.js
new file mode 100644
index 0000000..fab9daa
--- /dev/null
+++ b/src/blocks/sensing.js
@@ -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];
+};
\ No newline at end of file
diff --git a/src/blocks/text_rendering.js b/src/blocks/text_rendering.js
new file mode 100644
index 0000000..d99e383
--- /dev/null
+++ b/src/blocks/text_rendering.js
@@ -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`;
+};
\ No newline at end of file
diff --git a/src/functions/monitors.js b/src/functions/monitors.js
new file mode 100644
index 0000000..a17a71b
--- /dev/null
+++ b/src/functions/monitors.js
@@ -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);
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/functions/runCode.js b/src/functions/runCode.js
index d1af593..5daa228 100644
--- a/src/functions/runCode.js
+++ b/src/functions/runCode.js
@@ -436,6 +436,742 @@ export function runCodeWithFunctions({
sprite.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);
}
diff --git a/src/scripts/editor.js b/src/scripts/editor.js
index 201ffda..eadc121 100644
--- a/src/scripts/editor.js
+++ b/src/scripts/editor.js
@@ -24,11 +24,20 @@ import {
} from "../functions/extensionManager.js";
import { Thread } from "../functions/threads.js";
import { runCodeWithFunctions } from "../functions/runCode.js";
+import {
+ createMonitor,
+ removeMonitor,
+ updateAllMonitors,
+ getAllMonitors,
+ clearAllMonitors,
+ loadMonitors,
+ getMonitor
+} from "../functions/monitors.js";
import config from "../config";
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 });
@@ -40,6 +49,9 @@ let currentRoom = null;
let amHost = false;
let invitesEnabled = true;
let connectedUsers = [];
+let mouseX = 0;
+let mouseY = 0;
+let mouseCoordsFrozen = false;
const wrapper = document.getElementById("stage-wrapper");
const stageContainer = document.getElementById("stage");
@@ -94,8 +106,10 @@ function createPenGraphics() {
}
createPenGraphics();
-export let projectVariables = {};
-export let sprites = [];
+window.projectVariables = {};
+export const projectVariables = window.projectVariables;
+window.sprites = [];
+export const sprites = window.sprites;
export let activeSprite = null;
window.projectSounds = [];
window.projectCostumes = ["default"];
@@ -186,6 +200,13 @@ workspace.registerToolboxCategoryCallback("GLOBAL_VARIABLES", function (_) {
varField.setAttribute("name", "VAR");
varField.textContent = name;
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);
}
@@ -427,15 +448,19 @@ function renderSpriteInfo() {
const infoEl = document.getElementById("sprite-info");
if (!activeSprite) {
- infoEl.innerHTML = "Select a sprite to see its info.
";
+ infoEl.innerHTML = `
+ Select a sprite to see its info.
+ Mouse: ${mouseX}, ${mouseY} ${mouseCoordsFrozen ? 'π' : ''}
+ `;
} else {
const sprite = activeSprite.pixiSprite;
infoEl.innerHTML = `
- ${Math.round(sprite.x)}, ${Math.round(-sprite.y)}
- ${Math.round(sprite.angle)}ΒΊ
- size: ${Math.round(((sprite.scale.x + sprite.scale.y) / 2) * 100)}
-
+ ${Math.round(sprite.x)}, ${Math.round(-sprite.y)}
+ ${Math.round(sprite.angle)}ΒΊ
+ size: ${Math.round(((sprite.scale.x + sprite.scale.y) / 2) * 100)}
+
+ Mouse: ${mouseX}, ${mouseY} ${mouseCoordsFrozen ? 'π' : ''}
`;
}
}
@@ -1024,7 +1049,20 @@ function stopAllScripts() {
clearTimeout(spriteData.sayTimeout);
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() {
@@ -1103,6 +1141,11 @@ async function runCode() {
}
}
+// Add ticker for updating monitors
+app.ticker.add(() => {
+ updateAllMonitors();
+});
+
app.view.addEventListener("click", () => {
for (const entry of eventRegistry.stageClick) {
entry.cb();
@@ -1211,6 +1254,7 @@ export async function getProject() {
backdrops: backdropsData,
currentBackdrop: currentBackdrop,
projectName: projectName,
+ monitors: getAllMonitors().map(m => m.toJSON()),
};
}
@@ -1223,6 +1267,7 @@ async function saveProject() {
backdrops: [], // ADD THIS
currentBackdrop: currentBackdrop, // ADD THIS
projectName: projectName,
+ monitors: getAllMonitors().map(m => m.toJSON()),
};
const toUint8Array = base64 =>
Uint8Array.from(atob(base64), c => c.charCodeAt(0));
@@ -1516,14 +1561,20 @@ async function handleProjectData(data) {
for (const child of app.stage.removeChildren()) {
if (child.destroy) child.destroy({ children: true });
}
- sprites = [];
+ sprites.length = 0;
+ window.sprites = sprites;
if (!Array.isArray(data.sprites)) {
window.alert("No valid sprites found in file.");
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
window.projectCostumes = ["default"];
@@ -1677,6 +1728,27 @@ async function handleProjectData(data) {
// IMPORTANT: Update toolbox AFTER everything is loaded
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
setTimeout(() => {
@@ -1876,6 +1948,29 @@ window.addEventListener("resize", () => {
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 = "") {
input = input.trim();
return (
@@ -2741,3 +2836,16 @@ workspace.addChangeListener(event => {
workspace.updateAllFunctionCalls = () => {
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;
\ No newline at end of file