From 9c9c6b99b30031e67bf02b5b2cc0e5712e49be9c Mon Sep 17 00:00:00 2001 From: arc360 Date: Tue, 20 Jan 2026 16:50:04 -0600 Subject: [PATCH] wow thats a lot of changes --- editor.html | 68 ++++ ext.js | 101 ----- package-lock.json | 44 +++ package.json | 2 + src/blocks/event.js | 2 +- src/blocks/looks.js | 93 +++++ src/blocks/monitors.js | 90 +++++ src/blocks/sensing.js | 278 +++++++++++++ src/blocks/text_rendering.js | 77 ++++ src/functions/monitors.js | 172 ++++++++ src/functions/runCode.js | 736 +++++++++++++++++++++++++++++++++++ src/scripts/editor.js | 128 +++++- 12 files changed, 1679 insertions(+), 112 deletions(-) delete mode 100644 ext.js create mode 100644 src/blocks/monitors.js create mode 100644 src/blocks/sensing.js create mode 100644 src/blocks/text_rendering.js create mode 100644 src/functions/monitors.js diff --git a/editor.html b/editor.html index 4f0d405..6522ab3 100644 --- a/editor.html +++ b/editor.html @@ -307,6 +307,46 @@ + + + + Hello World! + + + + + + + + Arial + + + + + + + + + #ff0000 + + + + + + + 25 + + + + + + + 25 + + + + + @@ -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