Init
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# NeoIDE
|
||||
NeoIDE is a web-based platform that allows to create games or projects using visual block-coding, inspired by Scratch.
|
||||
|
||||
[Website](https://NeoIDE.vercel.app/) | [Documentation](https://NeoIDE-docs.vercel.app/)
|
||||
564
editor.html
Normal file
@@ -0,0 +1,564 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Editor - NeoIDE</title>
|
||||
|
||||
<link href="src/index.css" rel="stylesheet" />
|
||||
<link href="src/editor.css" rel="stylesheet" />
|
||||
|
||||
<meta name="author" content="ddededodediamante">
|
||||
<meta name="description" content="Create and share games using visual block-coding, inspired by Scratch.">
|
||||
<meta name="keywords"
|
||||
content="NeoIDE, block coding, Scratch, visual programming, game maker, coding for kids, online coding">
|
||||
<meta name="theme-color" content="#3b82f6">
|
||||
<meta property="og:title" content="NeoIDE">
|
||||
<meta property="og:image" content="icons/NeoIDE.svg">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="NeoIDE">
|
||||
<meta name="twitter:description" content="Create and share games using visual block-coding, inspired by Scratch.">
|
||||
<meta name="twitter:image" content="icons/NeoIDE.svg">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<img src="/icons/NeoIDE.svg" alt="NeoIDE logo" class="logo" onclick="location.href='/'" style="cursor: pointer;">
|
||||
<button id="theme-button">
|
||||
<i class="fa-solid fa-paintbrush"></i>
|
||||
Appearance
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" id="project-name-input" placeholder="Untitled Project" maxlength="50">
|
||||
</div>
|
||||
<div>
|
||||
<button id="save-button">
|
||||
<i class="fa-solid fa-file-arrow-up"></i>
|
||||
Save
|
||||
</button>
|
||||
<button id="load-button">
|
||||
<i class="fa-solid fa-file-arrow-down"></i>
|
||||
Load
|
||||
</button>
|
||||
<input type="file" id="load-input" class="hidden" accept=".neo, .neoz" />
|
||||
<button id="liveshare-button">
|
||||
<i class="fa-solid fa-share-from-square"></i>
|
||||
Live Share
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<div class="left-panel">
|
||||
<div class="tab-header">
|
||||
<button class="tab-button" data-tab="code">Code</button>
|
||||
<button class="tab-button inactive" data-tab="costumes">Costumes</button>
|
||||
<button class="tab-button inactive" data-tab="sounds">Sounds</button>
|
||||
</div>
|
||||
|
||||
<div id="code-tab" class="tab-content active">
|
||||
<div id="blocklyDiv"></div>
|
||||
</div>
|
||||
|
||||
<div id="costumes-tab" class="tab-content tab-section">
|
||||
<h2>Costumes</h2>
|
||||
<div id="costumes-list"></div>
|
||||
<input type="file" id="costume-upload" accept="image/*">
|
||||
</div>
|
||||
|
||||
<div id="sounds-tab" class="tab-content tab-section">
|
||||
<h2>Sounds</h2>
|
||||
<div id="sounds-list"></div>
|
||||
<input type="file" id="sound-upload" accept="audio/*" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div id="stage-div">
|
||||
<div id="stage-controls">
|
||||
<button id="run-button">
|
||||
<img src="icons/flag.svg">
|
||||
</button>
|
||||
<button id="stop-button">
|
||||
<img src="icons/stop.svg">
|
||||
</button>
|
||||
<button id="fullscreen-button">
|
||||
<img src="icons/fullscreen.svg">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="stage-wrapper">
|
||||
<div id="stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sprite-info">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<div id="sprites-section">
|
||||
<h3>Sprites</h3>
|
||||
<div id="sprites-list"></div>
|
||||
<div id="sprites-section-buttons">
|
||||
<button id="add-sprite-button" class="primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add Sprite
|
||||
</button>
|
||||
<button id="delete-sprite-button" class="danger">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
Delete Sprite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backdrops-section">
|
||||
<h3>Backdrops</h3>
|
||||
<div id="backdrops-list"></div>
|
||||
<div id="backdrops-section-buttons">
|
||||
<button id="add-backdrop-button" class="primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Add Backdrop
|
||||
</button>
|
||||
<button id="delete-backdrop-button" class="danger">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
Delete Backdrop
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="backdrop-upload" class="hidden" accept="image/*" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="extensions-popup hidden" id="extensions-popup">
|
||||
<header>
|
||||
<h2>Extensions List</h2>
|
||||
<button id="extensions-custom-button" class="orange">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Load Custom
|
||||
</button>
|
||||
<button class="danger" onclick="document.getElementById('extensions-popup').classList.add('hidden')">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
Close
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="extensions-list"></div>
|
||||
</div>
|
||||
|
||||
<xml id="toolbox" style="display: none">
|
||||
<category name="Events" colour="#ffc400">
|
||||
<block type="when_flag_clicked"></block>
|
||||
<block type="project_timer"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="when_key_clicked"></block>
|
||||
<block type="when_stage_clicked"></block>
|
||||
<block type="when_timer_reaches"></block>
|
||||
<block type="every_seconds"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="when_custom_event_triggered"></block>
|
||||
<block type="trigger_custom_event"></block>
|
||||
</category>
|
||||
|
||||
<category name="Control" colour="#FFAB19">
|
||||
<block type="wait_one_frame"></block>
|
||||
<block type="wait_block">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="controls_if"></block>
|
||||
<block type="controls_repeat_ext">
|
||||
<value name="TIMES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_whileUntil"></block>
|
||||
<block type="controls_flow_statements"></block>
|
||||
<block type="controls_stopscript"></block>
|
||||
<block type="controls_run_instantly"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="controls_thread_current"></block>
|
||||
<block type="controls_thread_create"></block>
|
||||
<block type="controls_thread_set_var">
|
||||
<value name="THREAD">
|
||||
<shadow type="controls_thread_current"></shadow>
|
||||
</value>
|
||||
<value name="NAME">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">name</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_thread_get_var">
|
||||
<value name="THREAD">
|
||||
<shadow type="controls_thread_current"></shadow>
|
||||
</value>
|
||||
<value name="NAME">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">name</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
|
||||
<category name="Functions" colour="#FF6680" custom="FUNCTIONS_CATEGORY"></category>
|
||||
|
||||
<sep></sep>
|
||||
|
||||
<category name="Motion" colour="#4C97FF">
|
||||
<block type="move_steps">
|
||||
<value name="STEPS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="goto_position">
|
||||
<value name="x">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="set_position">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="change_position">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="get_position"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="point_towards">
|
||||
<value name="x">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="angle_set">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">15</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="angle_turn">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">15</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="get_angle"></block>
|
||||
</category>
|
||||
|
||||
<category name="Looks" colour="#9966FF">
|
||||
<block type="looks_hide_sprite"></block>
|
||||
<block type="looks_show_sprite"></block>
|
||||
<block type="looks_isVisible"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="switch_backdrop"></block>
|
||||
<block type="say_message">
|
||||
<value name="MESSAGE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">Hello!</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="say_message_duration">
|
||||
<value name="MESSAGE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">Hello!</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="DURATION">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="switch_costume">
|
||||
<value name="COSTUME">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">default</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="get_costume_size"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="set_size">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="change_size">
|
||||
<value name="AMOUNT">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="get_sprite_scale"></block>
|
||||
</category>
|
||||
|
||||
<category name="Sounds" colour="#ff66ba">
|
||||
<block type="play_sound">
|
||||
<value name="name">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">hey</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="stop_sound">
|
||||
<value name="name">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">hey</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="stop_all_sounds"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="set_sound_property">
|
||||
<value name="value">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="get_sound_property"></block>
|
||||
</category>
|
||||
|
||||
<sep></sep>
|
||||
|
||||
<category name="Operators" colour="#59ba57">
|
||||
<label text="Logic"></label>
|
||||
|
||||
<block type="logic_compare"></block>
|
||||
<block type="logic_operation"></block>
|
||||
<block type="logic_negate"></block>
|
||||
<block type="logic_boolean"></block>
|
||||
<block type="logic_null"></block>
|
||||
<block type="logic_ternary"></block>
|
||||
|
||||
<label text="Math"></label>
|
||||
|
||||
<block type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</block>
|
||||
<block type="math_arithmetic"></block>
|
||||
<block type="math_single"></block>
|
||||
<block type="math_trig"></block>
|
||||
<block type="math_constant"></block>
|
||||
<block type="math_number_property"></block>
|
||||
<block type="math_round"></block>
|
||||
<block type="math_on_list"></block>
|
||||
<block type="math_modulo"></block>
|
||||
<block type="math_constrain">
|
||||
<value name="LOW">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="HIGH">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_int">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_float"></block>
|
||||
|
||||
<label text="Text"></label>
|
||||
|
||||
<block type="text"></block>
|
||||
<block type="text_join"></block>
|
||||
<block type="text_append">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_length"></block>
|
||||
<block type="text_isEmpty"></block>
|
||||
<block type="text_indexOf"></block>
|
||||
<block type="text_charAt"></block>
|
||||
<block type="text_getSubstring"></block>
|
||||
<block type="text_changeCase"></block>
|
||||
<block type="text_trim"></block>
|
||||
<block type="text_print"></block>
|
||||
<block type="text_prompt_ext"></block>
|
||||
</category>
|
||||
|
||||
<category name="System" colour="#5CB1D6">
|
||||
<block type="key_pressed"></block>
|
||||
<block type="all_keys_pressed"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="mouse_button_pressed"></block>
|
||||
<block type="mouse_over"></block>
|
||||
<block type="get_mouse_position"></block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="window_size"></block>
|
||||
</category>
|
||||
|
||||
<category name="Lists" colour="#e35340">
|
||||
<block type="lists_create_with">
|
||||
<mutation items="2"></mutation>
|
||||
</block>
|
||||
<block type="lists_repeat">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="lists_length"></block>
|
||||
<block type="lists_isEmpty"></block>
|
||||
<block type="lists_indexOf"></block>
|
||||
<block type="lists_find">
|
||||
<value name="method">
|
||||
<block type="logic_compare">
|
||||
<field name="OP">EQ</field>
|
||||
<value name="A">
|
||||
<block type="lists_filter_item"></block>
|
||||
</value>
|
||||
<value name="B">
|
||||
<block type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
</value>
|
||||
<block type="lists_getIndex_modified"></block>
|
||||
<block type="lists_setIndex_modified"></block>
|
||||
<block type="lists_getSublist"></block>
|
||||
<block type="lists_split">
|
||||
<value name="DELIM">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">,</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_merge"></block>
|
||||
<block type="lists_sort"></block>
|
||||
<block type="lists_filter">
|
||||
<value name="method">
|
||||
<block type="logic_compare">
|
||||
<field name="OP">EQ</field>
|
||||
<value name="A">
|
||||
<block type="lists_filter_item"></block>
|
||||
</value>
|
||||
<value name="B">
|
||||
<block type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="lists_foreach"></block>
|
||||
<block type="lists_filter_item"></block>
|
||||
</category>
|
||||
|
||||
<category name="Objects" colour="#ff8349">
|
||||
<block type="json_create_statement"></block>
|
||||
<block type="json_key_value_statement">
|
||||
<value name="KEY">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"></field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"></field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="50"></sep>
|
||||
<block type="json_has_key">
|
||||
<value name="KEY">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">key</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="json_get">
|
||||
<value name="KEY">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">key</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="json_set">
|
||||
<value name="KEY">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">key</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="json_delete">
|
||||
<value name="KEY">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">key</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="json_property_list"></block>
|
||||
<block type="json_parse"></block>
|
||||
<block type="json_clone"></block>
|
||||
</category>
|
||||
|
||||
<category name="Variables" colour="#FF8C1A" custom="GLOBAL_VARIABLES"></category>
|
||||
</xml>
|
||||
|
||||
<script type="module" src="src/scripts/editor.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
101
ext.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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);
|
||||
92
index.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Home - NeoIDE</title>
|
||||
|
||||
<link href="src/index.css" rel="stylesheet" />
|
||||
<link href="src/home.css" rel="stylesheet" />
|
||||
|
||||
<meta name="author" content="ddededodediamante" />
|
||||
<meta name="description" content="Create and share games using visual block-coding, inspired by Scratch." />
|
||||
<meta name="keywords"
|
||||
content="NeoIDE, block coding, Scratch, visual programming, game maker, coding for kids, online coding" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta property="og:title" content="NeoIDE" />
|
||||
<meta property="og:image" content="icons/NeoIDE.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="NeoIDE" />
|
||||
<meta name="twitter:description" content="Create and share games using visual block-coding, inspired by Scratch." />
|
||||
<meta name="twitter:image" content="icons/NeoIDE.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<img src="/icons/NeoIDE.svg" alt="NeoIDE logo" class="logo" />
|
||||
<button onclick="location.href='/editor'">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
Editor
|
||||
</button>
|
||||
<button id="theme-button">
|
||||
<i class="fa-solid fa-paintbrush"></i>
|
||||
Appearance
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="location.href='/login'" id="login-button">
|
||||
<i class="fa-solid fa-right-to-bracket"></i>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="about">
|
||||
<h2>What is NeoIDE?</h2>
|
||||
<p>
|
||||
NeoIDE is a web-based platform that allows to create games or projects
|
||||
using visual block-coding, inspired by Scratch.
|
||||
</p>
|
||||
|
||||
<h2>Features</h2>
|
||||
<div class="feature-cards">
|
||||
<div class="feature-card">
|
||||
<h3>Visual Block Editor</h3>
|
||||
<p>Write code by snapping blocks together, no typing required.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Custom Extensions</h3>
|
||||
<p>
|
||||
Extend your projects with custom blocks, new categories, and
|
||||
powerful features.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>Themes</h3>
|
||||
<p>Switch between light and dark modes to match your style.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-section">
|
||||
<h2>Ready to build your first project?</h2>
|
||||
<button class="orange large" onclick="location.href='/editor'">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
Launch the Editor
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
© 2025 NeoIDE. Made with ❤️ by
|
||||
<a href="https://github.com/ddededodediamante">ddededodediamante</a>.
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="src/scripts/index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
46
login.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login - NeoIDE</title>
|
||||
|
||||
<link href="src/index.css" rel="stylesheet" />
|
||||
<link href="src/login.css" rel="stylesheet" />
|
||||
|
||||
<meta name="author" content="ddededodediamante" />
|
||||
<meta name="description" content="Login to NeoIDE" />
|
||||
<meta name="keywords"
|
||||
content="NeoIDE, block coding, Scratch, visual programming, game maker, coding for kids, online coding" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta property="og:title" content="NeoIDE" />
|
||||
<meta property="og:image" content="icons/NeoIDE.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="NeoIDE" />
|
||||
<meta name="twitter:description" content="Login to NeoIDE" />
|
||||
<meta name="twitter:image" content="icons/NeoIDE.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="info">
|
||||
<img src="/icons/NeoIDE.svg" alt="NeoIDE logo" onclick="location.href='/'" style="cursor: pointer" />
|
||||
<a href="/signup">Create an account</a>
|
||||
|
||||
<p>Username</p>
|
||||
<input type="text" id="username" />
|
||||
|
||||
<p>Password</p>
|
||||
<input type="password" id="password" />
|
||||
|
||||
<button id="login">
|
||||
<i class="fa-solid fa-right-to-bracket"></i>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/scripts/login.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
2667
package-lock.json
generated
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "rarry-vite",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockly/plugin-strict-connection-checker": "^6.0.1",
|
||||
"@fortawesome/fontawesome-free": "^7.0.1",
|
||||
"blockly": "^12.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"pako": "^2.1.0",
|
||||
"pixi.js-legacy": "^7.4.3",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 112 KiB |
9
public/icons/NeoIDE.svg
Normal file
|
After Width: | Height: | Size: 121 KiB |
1
public/icons/R-Rarry.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="51" height="51" viewBox="0,0,51,51"><g transform="translate(-214.5,-154.5)"><g stroke-miterlimit="10"><path d="M214.5,205.5v-51h51v51z" fill="none" stroke="none" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M249.95672,193.07041c-1.38675,2.08013 -3.46687,0.69337 -3.46687,0.69337l-0.89978,-0.6313c-3.00902,-2.84407 -5.14493,-6.84463 -6.74625,-10.95894l-0.30141,0.15628l-2.27914,-4.3955c-0.19325,-0.3548 -0.30306,-0.76161 -0.30306,-1.19407c0,-1.38071 1.11929,-2.5 2.5,-2.5c0.2764,0 0.54232,0.04485 0.79088,0.12768l2.28077,0.70177c0.56515,-0.01794 1.14205,0.04184 1.68919,-0.09504c2.00262,-0.50105 2.03218,-2.70859 0.85435,-4.25513c-0.83886,-1.10146 -5.05538,-2.19849 -6.2545,-1.81827c-0.40717,0.12911 -0.26103,0.81403 -0.41693,1.21172c-0.40898,1.04326 -0.93243,2.04343 -1.27713,3.10966c-2.07743,6.42592 -1.51245,13.63653 -1.51245,20.30335c0,0 0,2.5 -2.5,2.5c-2.5,0 -2.5,-2.5 -2.5,-2.5c0,-7.23484 -0.50441,-14.89339 1.76281,-21.8657c0.44681,-1.37406 2.39864,-6.42075 4.03766,-7.14205c3.20218,-1.40921 10.37349,0.02227 12.57827,3.09462c3.27962,4.57013 2.07785,10.1684 -3.23973,12.12121c-0.56775,0.2085 -1.16802,0.2683 -1.77828,0.28245l0.00607,0.01171l-0.38242,0.1983c2.56204,2.87801 3.34968,6.70605 6.33614,9.18948l0.32845,0.18752c0,0 2.08013,1.38675 0.69338,3.46688z" fill="#3b82f6" stroke="#3b82f6" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M249.95672,193.07041c-1.38675,2.08013 -3.46687,0.69337 -3.46687,0.69337l-0.89978,-0.6313c-3.00902,-2.84407 -5.14493,-6.84463 -6.74625,-10.95894l-0.30141,0.15628l-2.27914,-4.3955c-0.19325,-0.3548 -0.30306,-0.76161 -0.30306,-1.19407c0,-1.38071 1.11929,-2.5 2.5,-2.5c0.2764,0 0.54232,0.04485 0.79088,0.12768l2.28077,0.70177c0.56515,-0.01794 1.14205,0.04184 1.68919,-0.09504c2.00262,-0.50105 2.03218,-2.70859 0.85435,-4.25513c-0.83886,-1.10146 -5.05538,-2.19849 -6.2545,-1.81827c-0.40717,0.12911 -0.26103,0.81403 -0.41693,1.21172c-0.40898,1.04326 -0.93243,2.04343 -1.27713,3.10966c-2.07743,6.42592 -1.51245,13.63653 -1.51245,20.30335c0,0 0,2.5 -2.5,2.5c-2.5,0 -2.5,-2.5 -2.5,-2.5c0,-7.23484 -0.50441,-14.89339 1.76281,-21.8657c0.44681,-1.37406 2.39864,-6.42075 4.03766,-7.14205c3.20218,-1.40921 10.37349,0.02227 12.57827,3.09462c3.27962,4.57013 2.07785,10.1684 -3.23973,12.12121c-0.56775,0.2085 -1.16802,0.2683 -1.77828,0.28245l0.00607,0.01171l-0.38242,0.1983c2.56204,2.87801 3.34968,6.70605 6.33614,9.18948l0.32845,0.18752c0,0 2.08013,1.38675 0.69338,3.46688z" fill="#3b82f6" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/><path d="M249.95672,193.07041c-1.38675,2.08013 -3.46687,0.69337 -3.46687,0.69337l-0.89978,-0.6313c-3.00902,-2.84407 -5.14493,-6.84463 -6.74625,-10.95894l-0.30141,0.15628l-2.27914,-4.3955c-0.19325,-0.3548 -0.30306,-0.76161 -0.30306,-1.19407c0,-1.38071 1.11929,-2.5 2.5,-2.5c0.2764,0 0.54232,0.04485 0.79088,0.12768l2.28077,0.70177c0.56515,-0.01794 1.14205,0.04184 1.68919,-0.09504c2.00262,-0.50105 2.03218,-2.70859 0.85435,-4.25513c-0.83886,-1.10146 -5.05538,-2.19849 -6.2545,-1.81827c-0.40717,0.12911 -0.26103,0.81403 -0.41693,1.21172c-0.40898,1.04326 -0.93243,2.04343 -1.27713,3.10966c-2.07743,6.42592 -1.51245,13.63653 -1.51245,20.30335c0,0 0,2.5 -2.5,2.5c-2.5,0 -2.5,-2.5 -2.5,-2.5c0,-7.23484 -0.50441,-14.89339 1.76281,-21.8657c0.44681,-1.37406 2.39864,-6.42075 4.03766,-7.14205c3.20218,-1.40921 10.37349,0.02227 12.57827,3.09462c3.27962,4.57013 2.07785,10.1684 -3.23973,12.12121c-0.56775,0.2085 -1.16802,0.2683 -1.77828,0.28245l0.00607,0.01171l-0.38242,0.1983c2.56204,2.87801 3.34968,6.70605 6.33614,9.18948l0.32845,0.18752c0,0 2.08013,1.38675 0.69338,3.46688z" fill="#3b82f6" stroke="#ffffff" stroke-width="0" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg><!--rotationCenter:25.49999999999997:25.5-->
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/icons/blur.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
public/icons/ddededodediamante.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
1
public/icons/flag.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="250.55943" height="319.51951" viewBox="0,0,250.55943,319.51951"><g transform="translate(-114.72028,-20.24026)"><g stroke="none" stroke-miterlimit="10"><path d="M137.94087,227.38855l-7.22058,-154.89476l208.68752,-35.88232l2.22058,157.89477z" fill="#00bd70" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M365.27971,55.55746v156.19631c0,5.81379 -3.44142,11.08087 -8.77195,13.4142c-5.33053,2.33821 -11.53485,1.29847 -15.80611,-2.6555c-12.40374,-11.45186 -24.41696,-16.78239 -37.80187,-16.78239c-17.72451,0 -36.22516,9.27961 -55.81438,19.11083c-21.77122,10.92954 -44.27954,22.22032 -68.93569,22.2252c-0.00488,0 -0.00977,0 -0.00977,0c-11.98881,0 -23.28446,-2.73361 -34.13102,-8.10319v86.15253c0,8.08855 -6.55577,14.64432 -14.64432,14.64432c-8.08854,0 -14.64432,-6.55578 -14.64432,-14.64432v-265.028c0,-1.98674 0.40516,-3.88075 1.12273,-5.60878c0.40028,-5.37447 3.69525,-10.14363 8.68897,-12.33052c5.33053,-2.33333 11.53484,-1.29358 15.8061,2.65063c12.40862,11.45186 24.42184,16.78239 37.80675,16.78239c17.72451,0 36.22028,-9.2845 55.80462,-19.11572c21.77122,-10.92954 44.27954,-22.22519 68.94058,-22.22519c20.91209,0 39.77885,8.03485 57.67909,24.55853c3.00209,2.77266 4.71059,6.66804 4.71059,10.75869zM335.99107,62.24503c-10.76846,-8.64991 -21.40511,-12.71615 -33.09616,-12.71615c-17.71963,0 -36.22028,9.28449 -55.80462,19.11571c-21.76634,10.92955 -44.27954,22.2252 -68.94057,22.2252c-11.5983,0 -22.5669,-2.47001 -33.09128,-7.49301v121.68942c10.76845,8.64991 21.40511,12.71615 33.0864,12.71615h0.00489c17.71963,-0.00488 36.2154,-9.2845 55.79974,-19.11084c21.77611,-10.92954 44.2893,-22.22519 68.95034,-22.22519c11.5983,0 22.56689,2.47001 33.09128,7.48813z" fill="#00995b" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg><!--rotationCenter:125.27971999999994:159.75974000000002-->
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
23
public/icons/fullscreen.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="67.26552" height="67.26552" viewBox="0,0,67.26552,67.26552">
|
||||
<g transform="translate(-206.36724,-146.36724)">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10">
|
||||
<path d="" stroke-width="5" stroke-linejoin="miter"/>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M230.28908,150.86724h-19.42184"/>
|
||||
<path d="M210.86724,150.86724v19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M249.71092,150.86724h19.42184"/>
|
||||
<path d="M269.13276,150.86724v19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M249.71092,209.13276h19.42184"/>
|
||||
<path d="M269.13276,209.13276v-19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M230.28908,209.13276h-19.42184"/>
|
||||
<path d="M210.86724,209.13276v-19.42184"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 903 B |
1
public/icons/left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="54.3723" height="54.3723" viewBox="0,0,54.3723,54.3723"><g transform="translate(-212.81385,-152.81385)"><g stroke="#ffffff" stroke-miterlimit="10"><path d="M228.76773,172.40672l-7.97986,6.24332l-0.93968,-10.08831z" fill="#ffffff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M212.81385,207.18615v-54.3723h54.3723v54.3723z" fill="none" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M223.4401,171.27877c3.13548,-5.94138 9.37476,-9.99105 16.5599,-9.99105c10.3345,0 18.71227,8.37777 18.71227,18.71227c0,10.3345 -8.37777,18.71227 -18.71227,18.71227c-7.02943,0 -13.15356,-3.87605 -16.35171,-9.60747" fill="none" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg><!--rotationCenter:27.186149999999998:27.186149999999998-->
|
||||
|
After Width: | Height: | Size: 906 B |
1
public/icons/pen.svg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
public/icons/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="271.93498" height="281.22102" viewBox="0,0,271.93498,281.22102"><g transform="translate(-104.03251,-39.38949)"><g fill="#3b82f6" stroke-linejoin="round" stroke-miterlimit="10"><path d="M358.46749,180l-236.93498,123.11051v-246.22102z" stroke="#3067dc" stroke-width="35" stroke-linecap="round"/><path d="M358.46749,180l-236.93498,123.11051v-246.22102z" stroke="#000000" stroke-width="0" stroke-linecap="butt"/></g></g></svg><!--rotationCenter:135.9674900000001:140.61051-->
|
||||
|
After Width: | Height: | Size: 575 B |
1
public/icons/right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="54.3723" height="54.3723" viewBox="0,0,54.3723,54.3723"><g transform="translate(-212.81385,-152.81385)"><g stroke="#ffffff" stroke-miterlimit="10"><path d="M260.15181,168.56173l-0.93968,10.08831l-7.97986,-6.24332z" fill="#ffffff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M212.81385,207.18615v-54.3723h54.3723v54.3723z" fill="none" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M256.35171,189.10479c-3.19815,5.73142 -9.32228,9.60747 -16.35171,9.60747c-10.3345,0 -18.71227,-8.37777 -18.71227,-18.71227c0,-10.3345 8.37777,-18.71227 18.71227,-18.71227c7.18514,0 13.42442,4.04967 16.5599,9.99105" fill="none" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg><!--rotationCenter:27.186149999999998:27.186149999999998-->
|
||||
|
After Width: | Height: | Size: 908 B |
1
public/icons/sets.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="251.1523" height="125.57615" viewBox="0,0,251.1523,125.57615"><defs><linearGradient x1="240" y1="117.21197" x2="240" y2="242.7881" gradientUnits="userSpaceOnUse" id="color-1"><stop offset="0" stop-color="#49dfc6"/><stop offset="1" stop-color="#2cc2a9"/></linearGradient></defs><g transform="translate(-114.42385,-117.21195)"><g stroke-miterlimit="10"><path d="M114.42385,242.7881v-125.57615h125.57615v0.00001h125.57615v125.57614z" fill="url(#color-1)" stroke="none" stroke-width="0" stroke-linecap="butt" stroke-linejoin="miter"/><path d="M272.52577,187.70602c-1.60773,7.72448 -5.48845,14.37715 -11.64217,19.958c-6.17219,5.59933 -13.33305,9.06426 -21.48257,10.39479c-2.14364,0.35111 -8.04788,0.52667 -17.71272,0.52667h-12.88954c-1.33053,-0.6283 -1.9958,-1.51533 -1.9958,-2.66107c0,-0.97942 0.57286,-1.86645 1.71861,-2.66107h13.832c9.84965,-0.0924 15.97564,-0.40655 18.37799,-0.94246c6.46787,-1.40445 12.07643,-4.32423 16.8257,-8.75934c4.74927,-4.43511 7.91852,-9.67409 9.50777,-15.71692c0.53591,-2.12515 0.80386,-3.7606 0.80386,-4.90634v-0.27719h-59.07014c-1.33053,-0.70223 -1.9958,-1.58925 -1.9958,-2.66107c0,-1.07181 0.66527,-1.95884 1.9958,-2.66107h59.07014v-0.27719c0,-1.14574 -0.26795,-2.78119 -0.80386,-4.90634c-1.58925,-6.11675 -4.74002,-11.32801 -9.45233,-15.63377c-4.69383,-4.30576 -10.36707,-7.29946 -17.01974,-8.9811c-2.30996,-0.44351 -8.38975,-0.71146 -18.23939,-0.80386l-13.832,-0.1386c-1.14574,-0.79462 -1.71861,-1.63545 -1.71861,-2.52247c0,-1.14574 0.66527,-2.03276 1.9958,-2.66107h13.16674c2.12515,0 4.34271,0 6.65267,0c2.30996,0 4.16716,0.01848 5.57161,0.05544c1.42294,0.05544 2.18059,0.08316 2.27299,0.08316c10.20076,1.05334 18.8677,5.09113 26.00084,12.1134c7.15162,7.00377 10.72742,15.78161 10.72742,26.33347c0,3.10458 -0.22176,5.67324 -0.66527,7.70601z" fill="#ffffff" stroke="#28a08c" stroke-width="12.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M272.52577,187.70601c-1.60773,7.72448 -5.48845,14.37715 -11.64217,19.958c-6.17219,5.59933 -13.33305,9.06426 -21.48257,10.39479c-2.14364,0.35111 -8.04788,0.52667 -17.71272,0.52667h-12.88954c-1.33053,-0.6283 -1.9958,-1.51533 -1.9958,-2.66107c0,-0.97942 0.57286,-1.86645 1.71861,-2.66107h13.832c9.84965,-0.0924 15.97564,-0.40655 18.37799,-0.94246c6.46787,-1.40445 12.07643,-4.32423 16.8257,-8.75934c4.74927,-4.43511 7.91852,-9.67409 9.50777,-15.71692c0.53591,-2.12515 0.80386,-3.7606 0.80386,-4.90634v-0.27719h-59.07014c-1.33053,-0.70223 -1.9958,-1.58925 -1.9958,-2.66107c0,-1.07181 0.66527,-1.95884 1.9958,-2.66107h59.07014v-0.27719c0,-1.14574 -0.26795,-2.78119 -0.80386,-4.90634c-1.58925,-6.11675 -4.74002,-11.32801 -9.45233,-15.63377c-4.69383,-4.30576 -10.36707,-7.29946 -17.01974,-8.9811c-2.30996,-0.44351 -8.38975,-0.71146 -18.23939,-0.80386l-13.832,-0.1386c-1.14574,-0.79462 -1.71861,-1.63545 -1.71861,-2.52247c0,-1.14574 0.66527,-2.03276 1.9958,-2.66107h13.16674c2.12515,0 4.34271,0 6.65267,0c2.30996,0 4.16716,0.01848 5.57161,0.05544c1.42294,0.05544 2.18059,0.08316 2.27299,0.08316c10.20076,1.05334 18.8677,5.09113 26.00084,12.1134c7.15162,7.00377 10.72742,15.78161 10.72742,26.33347c0,3.10458 -0.22176,5.67324 -0.66527,7.70601z" fill="#ffffff" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg><!--rotationCenter:125.57615:62.788045000000025-->
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
23
public/icons/smallscreen.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="67.26552" height="67.26552" viewBox="0,0,67.26552,67.26552">
|
||||
<g transform="translate(-206.36724,-146.36724)">
|
||||
<g fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10">
|
||||
<path d="" stroke-width="5" stroke-linejoin="miter"/>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M210.86724,170.28908h19.42184"/>
|
||||
<path d="M230.28908,170.28908l0,-19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M269.13276,170.28908h-19.42184"/>
|
||||
<path d="M249.71092,170.28908l0,-19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M269.13276,189.71092l-19.42184,0"/>
|
||||
<path d="M249.71092,189.71092l0,19.42184"/>
|
||||
</g>
|
||||
<g stroke-width="9" stroke-linejoin="round">
|
||||
<path d="M210.86724,189.71092l19.42184,0"/>
|
||||
<path d="M230.28908,189.71092v19.42184"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 913 B |
1
public/icons/statement.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97.01543" height="57" viewBox="0,0,97.01543,57"><g transform="translate(-191.49228,-151.5)"><g fill="#0fbd8c" stroke="#0c9770" stroke-miterlimit="10"><path d="M191.99229,156c0,-2.20914 1.79086,-4 4,-4h8c2,0 3,1 4,2l4,4c1,1 2,2 4,2h12c2,0 3,-1 4,-2l4,-4c1,-1 2,-2 4,-2h44.01543c2.20914,0 4,1.79086 4,4v40c0,2.20914 -1.79086,4 -4,4h-44.01543c-2,0 -3,1 -4,2l-4,4c-1,1 -2,2 -4,2h-12c-2,0 -3,-1 -4,-2l-4,-4c-1,-1 -2,-2 -4,-2h-8c-2.20914,0 -4,-1.79086 -4,-4z"/></g></g></svg><!--rotationCenter:48.50771499999999:28.5-->
|
||||
|
After Width: | Height: | Size: 617 B |
1
public/icons/stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="334.4865" height="334.48648" viewBox="0,0,334.4865,334.48648"><g transform="translate(-72.75678,-12.75678)"><g stroke-miterlimit="10"><path d="M93.63355,240.62569v-121.25568l85.74078,-85.74079h121.25138l85.74078,85.74509v121.25569l-85.74078,85.73647h-121.25138z" fill="#c82525" stroke="none" stroke-width="0.5"/><path d="M305.95669,339.24327h-131.91332l-93.28658,-93.28658v-131.91763l93.28228,-93.28227h131.92194l93.28228,93.28228v131.92194zM179.39347,326.3203h121.21311l85.71373,-85.70942v-121.21742l-85.71373,-85.71803h-121.21311l-85.71373,85.71372v121.21742z" fill="#911a1a" stroke="#911a1a" stroke-width="16"/></g></g></svg><!--rotationCenter:167.24321500000002:167.24321500000002-->
|
||||
|
After Width: | Height: | Size: 791 B |
1
public/icons/stopAudio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="271.93498" height="281.22102" viewBox="0,0,271.93498,281.22102"><g transform="translate(-104.03251,-39.38949)"><g fill="#3b82f6" stroke-linejoin="round" stroke-miterlimit="10"><path d="M121.53251,303.11051v-246.22102h69.01144v246.22102zM289.45606,303.11051v-246.22102h69.01144v246.22102z" stroke="#3067dc" stroke-width="35" stroke-linecap="round"/><path d="M121.53251,303.11051v-246.22102h69.01144v246.22102zM289.45606,303.11051v-246.22102h69.01144v246.22102z" stroke="none" stroke-width="NaN" stroke-linecap="butt"/></g></g></svg><!--rotationCenter:135.967485:140.61051000000003-->
|
||||
|
After Width: | Height: | Size: 686 B |
1
public/icons/terminal.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97.01543" height="49" viewBox="0,0,97.01543,49"><g transform="translate(-191.49228,-155.5)"><g fill="#0fbd8c" stroke="#0c9770" stroke-miterlimit="10"><path d="M191.99229,160c0,-2.20914 1.79086,-4 4,-4h8c2,0 3,1 4,2l4,4c1,1 2,2 4,2h12c2,0 3,-1 4,-2l4,-4c1,-1 2,-2 4,-2h44.01543c2.20914,0 4,1.79086 4,4v40c0,2.20914 -1.79086,4 -4,4h-88.01543c-2.20914,0 -4,-1.79086 -4,-4z"/></g></g></svg><!--rotationCenter:48.50771499999999:24.5-->
|
||||
|
After Width: | Height: | Size: 534 B |
1
public/icons/trash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="281.22102" height="310.72029" viewBox="0,0,281.22102,310.72029"><g transform="translate(-99.3895,-24.63988)"><g fill="#f63b3b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"><path d="M164.32736,317.86015c-12.07945,0 -21.87176,-9.7923 -21.87176,-21.87176v-180.6902h-14.83215c-5.92818,0 -10.73395,-4.40653 -10.73395,-9.8423v-33.80748c0,-5.43577 4.80573,-9.8423 10.73395,-9.8423h62.8177v-9.82393c0,-5.43577 4.02337,-9.8423 8.98645,-9.8423h81.14482c4.96308,0 8.98645,4.40653 8.98645,9.8423v9.82393h62.8177c5.92822,0 10.73395,4.40653 10.73395,9.8423v33.80749c0,5.43577 -4.80573,9.8423 -10.73395,9.8423h-14.83215v180.69021c0,12.07945 -9.7923,21.87176 -21.87176,21.87176zM304.9564,282.46097c4.52979,0 8.20192,-3.67213 8.20192,-8.20192v-118.89972c0,-4.52979 -3.67213,-8.20192 -8.20192,-8.20192h-11.91552c-4.52979,0 -8.20192,3.67213 -8.20192,8.20192v118.89972c0,4.52979 3.67213,8.20192 8.20192,8.20192zM186.95912,282.46097c4.52979,0 8.20191,-3.67213 8.20191,-8.20192v-118.89972c0,-4.52979 -3.67212,-8.20192 -8.20191,-8.20192h-11.91552c-4.52979,0 -8.20191,3.67213 -8.20191,8.20192v118.89972c0,4.52979 3.67212,8.20192 8.20191,8.20192zM245.95778,282.46097c4.52979,0 8.20192,-3.67213 8.20192,-8.20192v-118.89968c0,-4.52979 -3.67213,-8.20191 -8.20192,-8.20191h-11.91552c-4.52979,0 -8.20192,3.67212 -8.20192,8.20191v118.89969c0,4.52979 3.67213,8.20192 8.20192,8.20192z" stroke="#7c1e1e" stroke-width="35"/><path d="M164.32736,317.86015c-12.07946,0 -21.87176,-9.7923 -21.87176,-21.87176v-180.69021h-14.83215c-5.92818,0 -10.73395,-4.40653 -10.73395,-9.8423v-33.80748c0,-5.43577 4.80573,-9.8423 10.73395,-9.8423h62.8177v-9.82393c0,-5.43577 4.02337,-9.8423 8.98645,-9.8423h81.14482c4.96308,0 8.98645,4.40653 8.98645,9.8423v9.82393h62.8177c5.92821,0 10.73395,4.40653 10.73395,9.8423v33.80748c0,5.43577 -4.80573,9.8423 -10.73395,9.8423h-14.83215v180.69021c0,12.07946 -9.7923,21.87176 -21.87176,21.87176zM304.9564,282.46097c4.52979,0 8.20192,-3.67213 8.20192,-8.20192v-118.89972c0,-4.52979 -3.67213,-8.20192 -8.20192,-8.20192h-11.91552c-4.52979,0 -8.20192,3.67213 -8.20192,8.20192v118.89972c0,4.52979 3.67213,8.20192 8.20192,8.20192zM186.95912,282.46097c4.52979,0 8.20192,-3.67213 8.20192,-8.20192v-118.89972c0,-4.52979 -3.67213,-8.20192 -8.20192,-8.20192h-11.91552c-4.52979,0 -8.20192,3.67213 -8.20192,8.20192v118.89972c0,4.52979 3.67213,8.20192 8.20192,8.20192zM245.95777,282.46097c4.52979,0 8.20192,-3.67213 8.20192,-8.20192v-118.89969c0,-4.52979 -3.67213,-8.20192 -8.20192,-8.20192h-11.91552c-4.52979,0 -8.20192,3.67213 -8.20192,8.20192v118.89969c0,4.52979 3.67213,8.20192 8.20192,8.20192z" stroke="none" stroke-width="0"/></g></g></svg><!--rotationCenter:140.61049500000001:155.36012-->
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
public/icons/tween.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="251.1523" height="125.57615" viewBox="0,0,251.1523,125.57615"><defs><linearGradient x1="240" y1="117.21193" x2="240" y2="242.78808" gradientUnits="userSpaceOnUse" id="color-1"><stop offset="0" stop-color="#32a2c0"/><stop offset="1" stop-color="#326fbf"/></linearGradient><linearGradient x1="240.21015" y1="151.25886" x2="240.21015" y2="208.1335" gradientUnits="userSpaceOnUse" id="color-2"><stop offset="0" stop-color="#276e99"/><stop offset="1" stop-color="#286299"/></linearGradient></defs><g transform="translate(-114.42385,-117.21192)"><g stroke-miterlimit="10"><path d="M114.42385,242.78808v-125.57615h125.57615v0.00001h125.57615v125.57614z" fill="url(#color-1)" stroke="none" stroke-width="0" stroke-linecap="butt"/><path d="M135.93903,159.0377c0,-12.08605 9.79768,-21.88373 21.88373,-21.88373c12.08605,0 21.88373,9.79768 21.88373,21.88373c0,12.08605 -9.79768,21.88373 -21.88372,21.88373c-12.08605,0 -21.88372,-9.79768 -21.88372,-21.88372z" fill="#ffffff" stroke="#276e99" stroke-width="7.5" stroke-linecap="butt"/><path d="M178.77374,153.64152c22.3226,-4.48493 54.67784,-5.70379 54.62078,24.9394c-0.06264,33.63529 42.21159,31.54456 68.25204,27.06778" fill="none" stroke="url(#color-2)" stroke-width="12.5" stroke-linecap="round"/><path d="M300.19157,200.96231c0,-12.08605 9.79768,-21.88372 21.88372,-21.88372c12.08605,0 21.88373,9.79768 21.88373,21.88373c0,12.08605 -9.79768,21.88373 -21.88373,21.88373c-12.08605,0 -21.88372,-9.79768 -21.88372,-21.88373z" fill="#ffffff" stroke="#286299" stroke-width="7.5" stroke-linecap="butt"/><path d="M135.93903,159.0377c0,-12.08605 9.79768,-21.88373 21.88373,-21.88373c12.08605,0 21.88373,9.79768 21.88373,21.88373c0,12.08605 -9.79768,21.88373 -21.88372,21.88373c-12.08605,0 -21.88372,-9.79768 -21.88372,-21.88372z" fill="#ffffff" stroke="none" stroke-width="0" stroke-linecap="butt"/><path d="M178.77374,153.64152c22.3226,-4.48493 54.67784,-5.70379 54.62078,24.9394c-0.06264,33.63529 42.21159,31.54456 68.25204,27.06778" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round"/><path d="M300.19157,200.96231c0,-12.08605 9.79768,-21.88372 21.88372,-21.88372c12.08605,0 21.88373,9.79768 21.88373,21.88373c0,12.08605 -9.79768,21.88373 -21.88373,21.88373c-12.08605,0 -21.88372,-9.79768 -21.88372,-21.88373z" fill="#ffffff" stroke="#276e99" stroke-width="0" stroke-linecap="butt"/></g></g></svg><!--rotationCenter:125.57615:62.788075000000006-->
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
46
signup.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login - NeoIDE</title>
|
||||
|
||||
<link href="src/index.css" rel="stylesheet" />
|
||||
<link href="src/login.css" rel="stylesheet" />
|
||||
|
||||
<meta name="author" content="ddededodediamante" />
|
||||
<meta name="description" content="Login to NeoIDE" />
|
||||
<meta name="keywords"
|
||||
content="NeoIDE, block coding, Scratch, visual programming, game maker, coding for kids, online coding" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta property="og:title" content="NeoIDE" />
|
||||
<meta property="og:image" content="icons/NeoIDE.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="NeoIDE" />
|
||||
<meta name="twitter:description" content="Login to NeoIDE" />
|
||||
<meta name="twitter:image" content="icons/NeoIDE.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="info">
|
||||
<img src="/icons/NeoIDE.svg" alt="NeoIDE logo" onclick="location.href='/'" style="cursor: pointer" />
|
||||
<a href="/login">Login to existing account</a>
|
||||
|
||||
<p>Username</p>
|
||||
<input type="text" id="username" />
|
||||
|
||||
<p>Password</p>
|
||||
<input type="password" id="password" />
|
||||
|
||||
<button id="login">
|
||||
<i class="fa-solid fa-right-to-bracket"></i>
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="src/scripts/signup.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
187
src/blocks/control.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["wait_one_frame"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("wait one frame");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("control_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["wait_block"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT").setCheck("Number").appendField("wait");
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["seconds", "1000"],
|
||||
["milliseconds", "1"],
|
||||
]),
|
||||
"MENU"
|
||||
);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("control_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["wait_one_frame"] = function (block) {
|
||||
return `await waitOneFrame();\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["wait_block"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const duration =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const menu = block.getFieldValue("MENU") || 0;
|
||||
return `await wait(${duration} * ${+menu});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_thread_create"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("create thread");
|
||||
this.appendStatementInput("code").setCheck("default");
|
||||
this.setTooltip("Create and run the code specified in a new thread");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("control_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_thread_create"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const code = generator.statementToCode(block, "code");
|
||||
return `Thread.getCurrentContext().spawn(async () => {\n${code}});`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_thread_current"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("current thread");
|
||||
this.setOutput(true, "ThreadID");
|
||||
this.setStyle("control_blocks");
|
||||
this.setTooltip("Return the ID of the currently running thread");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_thread_current"] = () => [
|
||||
`Thread.getCurrentContext().id`,
|
||||
BlocklyJS.Order.MEMBER,
|
||||
];
|
||||
|
||||
Blockly.Blocks["controls_thread_set_var"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("NAME")
|
||||
.setCheck("String")
|
||||
.appendField("set variable");
|
||||
this.appendValueInput("VALUE").appendField("to");
|
||||
this.appendValueInput("THREAD")
|
||||
.setCheck("ThreadID")
|
||||
.appendField("in thread");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("control_blocks");
|
||||
this.setTooltip("Set a variable inside the given thread");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_thread_set_var"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const threadId =
|
||||
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
|
||||
const name =
|
||||
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
|
||||
const value =
|
||||
generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "undefined";
|
||||
return `Thread.set(${threadId}, ${name}, ${value});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_thread_get_var"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("NAME")
|
||||
.setCheck("String")
|
||||
.appendField("get variable");
|
||||
this.appendValueInput("THREAD")
|
||||
.setCheck("ThreadID")
|
||||
.appendField("from thread");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, null);
|
||||
this.setStyle("control_blocks");
|
||||
this.setTooltip("Get a variable from the given thread");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_thread_get_var"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const threadId =
|
||||
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
|
||||
const name =
|
||||
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
|
||||
const code = `Thread.get(${threadId}, ${name})`;
|
||||
return [code, BlocklyJS.Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_thread_has_var"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("NAME").setCheck("String").appendField("variable");
|
||||
this.appendValueInput("THREAD")
|
||||
.setCheck("ThreadID")
|
||||
.appendField("exists in thread");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, null);
|
||||
this.setStyle("control_blocks");
|
||||
this.setTooltip("Checks if a variable exists in the given thread");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_thread_has_var"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const threadId =
|
||||
generator.valueToCode(block, "THREAD", BlocklyJS.Order.NONE) || "null";
|
||||
const name =
|
||||
generator.valueToCode(block, "NAME", BlocklyJS.Order.NONE) || '""';
|
||||
const code = `Thread.has(${threadId}, ${name})`;
|
||||
return [code, BlocklyJS.Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_run_instantly"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("run instantly");
|
||||
this.appendStatementInput("do");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("control_blocks");
|
||||
this.setTooltip("Run inside code without frame delay");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_run_instantly"] = function (
|
||||
block
|
||||
) {
|
||||
const branch = BlocklyJS.javascriptGenerator.statementToCode(block, "do");
|
||||
return `let _prevFast = fastExecution;
|
||||
fastExecution = true;
|
||||
${branch}fastExecution = _prevFast;\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["controls_stopscript"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("stop this script");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setStyle("control_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["controls_stopscript"] = () =>
|
||||
'throw new Error("shouldStop");\n';
|
||||
172
src/blocks/event.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["when_flag_clicked"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("when")
|
||||
.appendField(
|
||||
new Blockly.FieldImage("icons/flag.svg", 25, 25, {
|
||||
alt: "Green flag",
|
||||
flipRtl: "FALSE",
|
||||
})
|
||||
)
|
||||
.appendField("clicked");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["when_flag_clicked"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("flag", null, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
const normalKeys = [
|
||||
..."abcdefghijklmnopqrstuvwxyz",
|
||||
..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(),
|
||||
];
|
||||
|
||||
Blockly.Blocks["when_key_clicked"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("when")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["any", "any"],
|
||||
["space", " "],
|
||||
["enter", "Enter"],
|
||||
["escape", "Escape"],
|
||||
["up arrow", "ArrowUp"],
|
||||
["down arrow", "ArrowDown"],
|
||||
["left arrow", "ArrowLeft"],
|
||||
["right arrow", "ArrowRight"],
|
||||
...normalKeys.map((i) => [i, i]),
|
||||
]),
|
||||
"KEY"
|
||||
)
|
||||
.appendField("key pressed");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["when_key_clicked"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const key = block.getFieldValue("KEY");
|
||||
const safeKey = generator.quote_(key);
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("key", ${safeKey}, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["when_stage_clicked"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("when stage clicked");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["when_stage_clicked"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("stageClick", null, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["project_timer"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("project timer");
|
||||
this.setOutput(true, "Number");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["project_timer"] = function (block) {
|
||||
return ["projectTime()", BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["when_timer_reaches"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("when timer reaches")
|
||||
.appendField(new Blockly.FieldNumber(2, 0), "VALUE")
|
||||
.appendField("seconds");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["when_timer_reaches"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const value = block.getFieldValue("VALUE");
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("timer", ${value}, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["every_seconds"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("every")
|
||||
.appendField(new Blockly.FieldNumber(2, 0.1), "SECONDS")
|
||||
.appendField("seconds");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["every_seconds"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const seconds = block.getFieldValue("SECONDS");
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("interval", ${seconds}, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["when_custom_event_triggered"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("when")
|
||||
.appendField(new Blockly.FieldTextInput("event_name"), "EVENT")
|
||||
.appendField("triggered");
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["when_custom_event_triggered"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const event = generator.quote_(block.getFieldValue("EVENT"));
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
return `registerEvent("custom", ${event}, async () => {\n${branch}});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["trigger_custom_event"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("trigger")
|
||||
.appendField(new Blockly.FieldTextInput("event_name"), "EVENT");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("events_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["trigger_custom_event"] = function (
|
||||
block
|
||||
) {
|
||||
const event = BlocklyJS.javascriptGenerator.quote_(block.getFieldValue("EVENT"));
|
||||
return `triggerCustomEvent(${event});\n`;
|
||||
};
|
||||
811
src/blocks/functions.js
Normal file
@@ -0,0 +1,811 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
const ARG_BLOCK_TYPE = "FunctionsArgumentBlock";
|
||||
|
||||
class CustomChecker extends Blockly.ConnectionChecker {
|
||||
canConnect(a, b, isDragging, opt_distance) {
|
||||
if (!isDragging) {
|
||||
return super.canConnect(a, b, isDragging, opt_distance);
|
||||
}
|
||||
|
||||
const existing = b.targetConnection && b.targetConnection.getSourceBlock();
|
||||
|
||||
if (
|
||||
existing &&
|
||||
existing.type === "functions_argument_block" &&
|
||||
existing.isShadow()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.canConnect(a, b, isDragging, opt_distance);
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.CONNECTION_CHECKER,
|
||||
"CustomChecker",
|
||||
CustomChecker,
|
||||
true
|
||||
);
|
||||
|
||||
class DuplicateOnDrag {
|
||||
constructor(block) {
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
isMovable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
startDrag(e) {
|
||||
const ws = this.block.workspace;
|
||||
|
||||
let typeToCreate = this.block.type;
|
||||
if (this.block.argType_ === "statement") {
|
||||
typeToCreate = "functions_statement_argument_block";
|
||||
}
|
||||
|
||||
let data = this.block.toCopyData();
|
||||
if (data?.blockState) {
|
||||
data.blockState.type = typeToCreate;
|
||||
} else {
|
||||
data.blockState = { type: typeToCreate };
|
||||
}
|
||||
|
||||
if (this.block.mutationToDom) {
|
||||
const mutation = this.block.mutationToDom();
|
||||
if (mutation) {
|
||||
data.blockState.extraState = mutation.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
this.copy = Blockly.clipboard.paste(data, ws);
|
||||
this.baseStrat = new Blockly.dragging.BlockDragStrategy(this.copy);
|
||||
this.copy.setDragStrategy(this.baseStrat);
|
||||
this.baseStrat.startDrag(e);
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
this.block.workspace
|
||||
.getGesture(e)
|
||||
.getCurrentDragger()
|
||||
.setDraggable(this.copy);
|
||||
this.baseStrat.drag(e);
|
||||
}
|
||||
|
||||
endDrag(e) {
|
||||
this.baseStrat?.endDrag(e);
|
||||
}
|
||||
|
||||
revertDrag(e) {
|
||||
this.copy?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function typeToBlocklyCheck(type) {
|
||||
return (
|
||||
{
|
||||
string: "String",
|
||||
number: "Number",
|
||||
boolean: "Boolean",
|
||||
array: "Array",
|
||||
object: "Object",
|
||||
}[type] || null
|
||||
);
|
||||
}
|
||||
|
||||
function findDuplicateArgNames(types, names) {
|
||||
const used = {};
|
||||
const duplicates = [];
|
||||
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
const key = types[i] + ":" + names[i];
|
||||
if (!names[i]) continue;
|
||||
|
||||
if (used[key]) duplicates.push(i);
|
||||
else used[key] = true;
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
function isValidIdentifier(name) {
|
||||
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
|
||||
}
|
||||
|
||||
Blockly.Blocks["functions_argument_block"] = {
|
||||
init() {
|
||||
if (!this.argType_) this.argType_ = "string";
|
||||
if (!this.argName_) this.argName_ = "arg";
|
||||
|
||||
this.setStyle("procedure_blocks");
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldLabel(this.argName_),
|
||||
"ARG_NAME"
|
||||
);
|
||||
|
||||
this.setOutput(true, null);
|
||||
this.setMovable(true);
|
||||
this.setDeletable(true);
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.setDragStrategy && this.isShadow()) {
|
||||
this.setDragStrategy(new DuplicateOnDrag(this));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("type", this.argType_ || "string");
|
||||
container.setAttribute("name", this.argName_ || "arg");
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const type = xmlElement.getAttribute("type") || "string";
|
||||
const name = xmlElement.getAttribute("name") || "arg";
|
||||
this.updateType_(type);
|
||||
this.updateName_(name);
|
||||
},
|
||||
|
||||
updateType_: function (type) {
|
||||
this.argType_ = type;
|
||||
if (type === "statement") {
|
||||
this.setOutputShape(3);
|
||||
this.setOutput(true, ARG_BLOCK_TYPE);
|
||||
} else {
|
||||
const outputType = typeToBlocklyCheck(type) || "String";
|
||||
this.setOutput(true, [outputType, ARG_BLOCK_TYPE]);
|
||||
}
|
||||
},
|
||||
|
||||
updateName_: function (name) {
|
||||
this.argName_ = name;
|
||||
if (this.getField("ARG_NAME")) {
|
||||
this.setFieldValue(name, "ARG_NAME");
|
||||
} else {
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldLabel(name),
|
||||
"ARG_NAME"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_statement_argument_block"] = {
|
||||
init() {
|
||||
if (!this.argName_) this.argName_ = "arg";
|
||||
|
||||
this.setStyle("procedure_blocks");
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldLabel(this.argName_),
|
||||
"ARG_NAME"
|
||||
);
|
||||
|
||||
this.setNextStatement(true, "default");
|
||||
this.setPreviousStatement(true, "default");
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("name", this.argName_ || "arg");
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const name = xmlElement.getAttribute("name") || "arg";
|
||||
this.updateName_(name);
|
||||
},
|
||||
|
||||
updateName_: function (name) {
|
||||
this.argName_ = name;
|
||||
if (this.getField("ARG_NAME")) {
|
||||
this.setFieldValue(name, "ARG_NAME");
|
||||
} else {
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldLabel(name),
|
||||
"ARG_NAME"
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_definition"] = {
|
||||
init: function () {
|
||||
this.setStyle("procedure_blocks");
|
||||
this.setTooltip("Function definition with a variable number of inputs.");
|
||||
this.setInputsInline(true);
|
||||
|
||||
this.functionId_ = Blockly.utils.idGenerator.genUid();
|
||||
this.itemCount_ = 0;
|
||||
this.argTypes_ = [];
|
||||
this.argNames_ = [];
|
||||
this.blockShape_ = "statement";
|
||||
this.returnTypes_ = [];
|
||||
|
||||
this.updateShape_();
|
||||
this.setMutator(
|
||||
new Blockly.icons.MutatorIcon(["functions_args_generic"], this)
|
||||
);
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("functionid", this.functionId_);
|
||||
container.setAttribute("items", String(this.itemCount_));
|
||||
container.setAttribute("shape", this.blockShape_ || "statement");
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const item = Blockly.utils.xml.createElement("item");
|
||||
item.setAttribute("type", this.argTypes_[i]);
|
||||
item.setAttribute("name", this.argNames_[i]);
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const items = xmlElement.getAttribute("items");
|
||||
this.itemCount_ = items ? parseInt(items, 10) : 0;
|
||||
this.argTypes_ = [];
|
||||
this.argNames_ = [];
|
||||
|
||||
const children = [...xmlElement.children].filter(
|
||||
n => n.tagName.toLowerCase() === "item"
|
||||
);
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
this.argTypes_[i] = children[i].getAttribute("type");
|
||||
this.argNames_[i] = children[i].getAttribute("name");
|
||||
}
|
||||
|
||||
while (this.argTypes_.length < this.itemCount_)
|
||||
this.argTypes_.push("label");
|
||||
while (this.argNames_.length < this.itemCount_) this.argNames_.push("text");
|
||||
|
||||
this.functionId_ =
|
||||
xmlElement.getAttribute("functionid") ||
|
||||
Blockly.utils.idGenerator.genUid();
|
||||
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
saveExtraState: function () {
|
||||
return {
|
||||
functionId: this.functionId_,
|
||||
itemCount: this.itemCount_,
|
||||
argTypes: this.argTypes_,
|
||||
argNames: this.argNames_,
|
||||
shape: this.blockShape_,
|
||||
returnTypes: this.returnTypes_,
|
||||
};
|
||||
},
|
||||
|
||||
loadExtraState: function (state) {
|
||||
this.functionId_ = state.functionId || Blockly.utils.idGenerator.genUid();
|
||||
this.itemCount_ = state.itemCount || 0;
|
||||
this.argTypes_ = state.argTypes || [];
|
||||
this.argNames_ = state.argNames || [];
|
||||
this.blockShape_ = state.shape || "statement";
|
||||
this.returnTypes_ = state.returnTypes || [];
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
createDefaultArgBlock_: function (type, name = "arg") {
|
||||
Blockly.Events.disable();
|
||||
|
||||
let block;
|
||||
try {
|
||||
const ws = this.workspace;
|
||||
block = ws.newBlock("functions_argument_block");
|
||||
block.setShadow(true);
|
||||
block.setEditable(false);
|
||||
block.updateType_(type);
|
||||
block.updateName_(name);
|
||||
|
||||
if (ws?.rendered) {
|
||||
block.initSvg();
|
||||
block.render();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
Blockly.Events.enable();
|
||||
return block;
|
||||
},
|
||||
|
||||
updateShape_: function () {
|
||||
let savedBody = null;
|
||||
|
||||
const bodyInput = this.getInput("BODY");
|
||||
if (bodyInput && bodyInput.connection?.targetConnection) {
|
||||
savedBody = bodyInput.connection.targetConnection;
|
||||
}
|
||||
|
||||
if (bodyInput) this.removeInput("BODY");
|
||||
if (this.getInput("EMPTY")) this.removeInput("EMPTY");
|
||||
if (this.getInput("SHAPE")) this.removeInput("SHAPE");
|
||||
|
||||
[...this.inputList].forEach(input => {
|
||||
const connection = input.connection?.targetConnection;
|
||||
if (connection) connection.getSourceBlock()?.dispose(false);
|
||||
this.removeInput(input.name);
|
||||
});
|
||||
|
||||
let firstArgAdded = this.argTypes_[0] === "label";
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const type = this.argTypes_[i];
|
||||
const name = this.argNames_[i];
|
||||
|
||||
if (type === "label") {
|
||||
this.appendDummyInput().appendField(new Blockly.FieldLabel(name));
|
||||
} else {
|
||||
const input = this.appendValueInput(name).setCheck(
|
||||
typeToBlocklyCheck(type)
|
||||
);
|
||||
|
||||
if (!firstArgAdded) {
|
||||
input.appendField("my block with");
|
||||
firstArgAdded = true;
|
||||
}
|
||||
|
||||
const reporter = this.createDefaultArgBlock_(type, name);
|
||||
reporter.setFieldValue(name, "ARG_NAME");
|
||||
|
||||
try {
|
||||
reporter.outputConnection.connect(input.connection);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.itemCount_ === 0) {
|
||||
this.appendDummyInput("EMPTY").appendField("my block");
|
||||
}
|
||||
|
||||
const newBody = this.appendStatementInput("BODY").setCheck("default");
|
||||
if (savedBody) {
|
||||
try {
|
||||
newBody.connection.connect(savedBody);
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
|
||||
decompose: function (workspace) {
|
||||
const containerBlock = workspace.newBlock("functions_args_container");
|
||||
if (workspace.rendered) containerBlock.initSvg();
|
||||
let connection = containerBlock.getInput("STACK").connection;
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const type = this.argTypes_[i] || "label";
|
||||
const name = this.argNames_[i] || "text";
|
||||
const itemBlock = workspace.newBlock("functions_args_generic");
|
||||
itemBlock.setFieldValue(type, "ARG_TYPE");
|
||||
itemBlock.setFieldValue(name, "ARG_NAME");
|
||||
if (workspace.rendered) itemBlock.initSvg();
|
||||
itemBlock.valueConnection_ = null;
|
||||
|
||||
connection.connect(itemBlock.previousConnection);
|
||||
connection = itemBlock.nextConnection;
|
||||
}
|
||||
|
||||
containerBlock.setFieldValue(this.blockShape_, "SHAPEMENU");
|
||||
|
||||
return containerBlock;
|
||||
},
|
||||
|
||||
compose: function (containerBlock) {
|
||||
const newTypes = [];
|
||||
const newNames = [];
|
||||
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
while (itemBlock) {
|
||||
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
|
||||
const type = itemBlock.getFieldValue("ARG_TYPE");
|
||||
const name = itemBlock.getFieldValue("ARG_NAME");
|
||||
newTypes.push(type);
|
||||
newNames.push(name);
|
||||
}
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
}
|
||||
|
||||
const dups = findDuplicateArgNames(newTypes, newNames);
|
||||
|
||||
const invalid = [];
|
||||
for (let i = 0; i < newTypes.length; i++) {
|
||||
const type = newTypes[i];
|
||||
const name = newNames[i];
|
||||
if (type !== "label") {
|
||||
if (!isValidIdentifier(name)) {
|
||||
invalid.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
let index = 0;
|
||||
while (itemBlock) {
|
||||
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
|
||||
if (dups.includes(index)) {
|
||||
itemBlock.setWarningText(
|
||||
"This argument name is already used for this type."
|
||||
);
|
||||
} else if (invalid.includes(index)) {
|
||||
itemBlock.setWarningText("This argument name is not a valid.");
|
||||
} else {
|
||||
itemBlock.setWarningText(null);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
}
|
||||
|
||||
const newBlockShape =
|
||||
containerBlock.getFieldValue("SHAPEMENU") || "statement";
|
||||
|
||||
if (dups.length > 0 || invalid.length > 0) return;
|
||||
|
||||
this.itemCount_ = newTypes.length;
|
||||
this.argTypes_ = newTypes;
|
||||
this.argNames_ = newNames;
|
||||
this.blockShape_ = newBlockShape;
|
||||
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
saveConnections: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
let i = 0;
|
||||
while (itemBlock) {
|
||||
if (!(itemBlock.isInsertionMarker && itemBlock.isInsertionMarker())) {
|
||||
const key = this.argTypes_[i] + "_" + this.argNames_[i];
|
||||
const input = this.getInput(key);
|
||||
itemBlock.valueConnection_ =
|
||||
input && input.connection && input.connection.targetConnection;
|
||||
i++;
|
||||
}
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
}
|
||||
},
|
||||
|
||||
updateReturnState_: function () {
|
||||
const body = this.getInputTargetBlock("BODY");
|
||||
const types = new Set();
|
||||
|
||||
function walk(block) {
|
||||
if (!block) return;
|
||||
|
||||
if (block?.childBlocks_?.length > 0) block?.childBlocks_.forEach(walk);
|
||||
|
||||
if (block.type === "functions_return") {
|
||||
const val = block.getInputTargetBlock("VALUE");
|
||||
const checks = val?.outputConnection?.check;
|
||||
if (checks !== undefined) {
|
||||
(Array.isArray(checks) ? checks : [checks]).forEach(t =>
|
||||
types.add(t)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
walk(block.getNextBlock());
|
||||
}
|
||||
walk(body);
|
||||
|
||||
if (types.size === 0) this.returnTypes_ = [];
|
||||
else this.returnTypes_ = [...types];
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_args_container"] = {
|
||||
init: function () {
|
||||
this.setStyle("procedure_blocks");
|
||||
this.appendDummyInput().appendField("arguments");
|
||||
this.appendStatementInput("STACK");
|
||||
this.appendDummyInput()
|
||||
.appendField("shape")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
[
|
||||
{
|
||||
src: "icons/statement.svg",
|
||||
width: 98 * 0.6,
|
||||
height: 57 * 0.6,
|
||||
alt: "A block with top and bottom connections",
|
||||
},
|
||||
"statement",
|
||||
],
|
||||
[
|
||||
{
|
||||
src: "icons/terminal.svg",
|
||||
width: 98 * 0.6,
|
||||
height: 48 * 0.6,
|
||||
alt: "A block with only a top connection",
|
||||
},
|
||||
"terminal",
|
||||
],
|
||||
]),
|
||||
"SHAPEMENU"
|
||||
);
|
||||
this.contextMenu = false;
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_args_generic"] = {
|
||||
init() {
|
||||
this.setStyle("procedure_blocks");
|
||||
|
||||
this.appendDummyInput()
|
||||
.appendField("argument")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["label", "label"],
|
||||
["string", "string"],
|
||||
["number", "number"],
|
||||
["boolean", "boolean"],
|
||||
["array", "array"],
|
||||
["object", "object"],
|
||||
["statement", "statement"],
|
||||
]),
|
||||
"ARG_TYPE"
|
||||
)
|
||||
.appendField(new Blockly.FieldTextInput("arg"), "ARG_NAME");
|
||||
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.contextMenu = false;
|
||||
this.valueConnection_ = null;
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_call"] = {
|
||||
init: function () {
|
||||
this.setStyle("procedure_blocks");
|
||||
this.setInputsInline(true);
|
||||
|
||||
this.functionId_ = null;
|
||||
this.blockShape_ = null;
|
||||
this.argTypes_ = [];
|
||||
this.argNames_ = [];
|
||||
this.previousArgTypes_ = [];
|
||||
this.previousArgNames_ = [];
|
||||
this.returnTypes_ = [];
|
||||
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("functionid", this.functionId_);
|
||||
container.setAttribute("items", this.argTypes_.length);
|
||||
container.setAttribute("shape", this.blockShape_ || "statement");
|
||||
container.setAttribute(
|
||||
"returntypes",
|
||||
JSON.stringify(this.returnTypes_ || [])
|
||||
);
|
||||
|
||||
for (let i = 0; i < this.argTypes_.length; i++) {
|
||||
const item = Blockly.utils.xml.createElement("item");
|
||||
item.setAttribute("type", this.argTypes_[i]);
|
||||
item.setAttribute("name", this.argNames_[i]);
|
||||
container.appendChild(item);
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
this.functionId_ = xmlElement.getAttribute("functionid");
|
||||
this.blockShape_ = xmlElement.getAttribute("shape") || "statement";
|
||||
this.previousArgTypes_ = [...this.argTypes_];
|
||||
this.previousArgNames_ = [...this.argNames_];
|
||||
this.argTypes_ = [];
|
||||
this.argNames_ = [];
|
||||
|
||||
this.returnTypes_;
|
||||
try {
|
||||
this.returnTypes_ = JSON.parse(
|
||||
xmlElement.getAttribute("returntypes") || "[]"
|
||||
);
|
||||
} catch {
|
||||
this.returnTypes_ = [];
|
||||
}
|
||||
|
||||
const items = parseInt(xmlElement.getAttribute("items") || "0", 10);
|
||||
for (let i = 0; i < items; i++) {
|
||||
const item = xmlElement.children[i];
|
||||
this.argTypes_[i] = item.getAttribute("type");
|
||||
this.argNames_[i] = item.getAttribute("name");
|
||||
}
|
||||
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
matchDefinition: function (defBlock) {
|
||||
this.functionId_ = defBlock.functionId_;
|
||||
this.previousArgTypes_ = [...this.argTypes_];
|
||||
this.previousArgNames_ = [...this.argNames_];
|
||||
this.argTypes_ = [...defBlock.argTypes_];
|
||||
this.argNames_ = [...defBlock.argNames_];
|
||||
this.blockShape_ = defBlock.blockShape_;
|
||||
this.returnTypes_ = [...defBlock.returnTypes_];
|
||||
|
||||
this.updateShape_();
|
||||
if (defBlock.workspace.rendered) this.render();
|
||||
},
|
||||
|
||||
updateShape_: function () {
|
||||
const oldConnections = {};
|
||||
|
||||
[...this.inputList].forEach(input => {
|
||||
if (input.connection && input.connection.targetBlock()) {
|
||||
oldConnections[input.name] = input.connection.targetConnection;
|
||||
}
|
||||
this.removeInput(input.name);
|
||||
});
|
||||
|
||||
const shape = this.blockShape_ || "statement";
|
||||
const nextConn = this.nextConnection;
|
||||
const prevConn = this.previousConnection;
|
||||
const outputConn = this.outputConnection;
|
||||
const returnTypes = this.returnTypes_ || [];
|
||||
|
||||
if (returnTypes?.length > 0) {
|
||||
if (prevConn && prevConn.isConnected()) {
|
||||
const blockAbove = prevConn.targetBlock();
|
||||
blockAbove.unplug(true);
|
||||
}
|
||||
if (nextConn && nextConn.isConnected()) {
|
||||
const blockBelow = nextConn.targetBlock();
|
||||
blockBelow.unplug(true);
|
||||
}
|
||||
|
||||
this.setPreviousStatement(false);
|
||||
this.setNextStatement(false);
|
||||
this.setOutput(true, returnTypes);
|
||||
} else {
|
||||
if (outputConn && outputConn.isConnected()) {
|
||||
outputConn.disconnect();
|
||||
}
|
||||
|
||||
if (shape === "statement") {
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setOutput(false);
|
||||
} else if (shape === "terminal") {
|
||||
if (nextConn && nextConn.isConnected()) {
|
||||
nextConn.targetBlock().unplug(true);
|
||||
}
|
||||
|
||||
this.setNextStatement(false);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setOutput(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.argTypes_ || this.argTypes_.length === 0) {
|
||||
this.appendDummyInput("EMPTY").appendField("my block");
|
||||
return;
|
||||
}
|
||||
|
||||
let firstLabel = this.argTypes_[0] === "label";
|
||||
|
||||
for (let i = 0; i < this.argTypes_.length; i++) {
|
||||
const type = this.argTypes_[i];
|
||||
const name = this.argNames_[i];
|
||||
|
||||
if (!type || !name) continue;
|
||||
|
||||
if (type === "label") {
|
||||
this.appendDummyInput().appendField(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!firstLabel) {
|
||||
this.appendDummyInput().appendField("my block with");
|
||||
firstLabel = true;
|
||||
}
|
||||
|
||||
let input;
|
||||
const key = type + "_" + name;
|
||||
if (type === "statement") {
|
||||
input = this.appendStatementInput(key).setCheck("default");
|
||||
} else {
|
||||
input = this.appendValueInput(key).setCheck(typeToBlocklyCheck(type));
|
||||
}
|
||||
|
||||
if (oldConnections[key]) {
|
||||
try {
|
||||
input.connection.connect(
|
||||
oldConnections[key].targetBlock()?.outputConnection ||
|
||||
oldConnections[key]
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["functions_return"] = {
|
||||
init() {
|
||||
this.setStyle("procedure_blocks");
|
||||
this.appendValueInput("VALUE").appendField("return");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(false);
|
||||
this.setInputsInline(true);
|
||||
},
|
||||
|
||||
update_() {
|
||||
const def = this.getSurroundParent();
|
||||
if (!def || def.type !== "functions_definition") return;
|
||||
|
||||
def.updateReturnState_();
|
||||
def.workspace.updateAllFunctionCalls();
|
||||
},
|
||||
|
||||
onchange(e) {
|
||||
if (e.isUiEvent || e.isBlank) return;
|
||||
|
||||
this.update_();
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["functions_argument_block"] = block => [
|
||||
block.argType_ + "_" + block.argName_,
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["functions_statement_argument_block"] =
|
||||
block => "statement_" + block.argName_ + "();\n";
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["functions_definition"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const params = block.argTypes_
|
||||
.map((type, i) => {
|
||||
if (type === "label") return null;
|
||||
return type + "_" + block.argNames_[i];
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const body = BlocklyJS.javascriptGenerator.statementToCode(block, "BODY");
|
||||
return `MyFunctions[${generator.quote_(
|
||||
block.functionId_
|
||||
)}] = async (${params.join(", ")}) => {\n${body}};\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["functions_call"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const args = [];
|
||||
|
||||
for (let i = 0; i < block.argTypes_.length; i++) {
|
||||
const type = block.argTypes_[i];
|
||||
const name = block.argNames_[i];
|
||||
const key = `${type}_${name}`;
|
||||
|
||||
if (type === "label") continue;
|
||||
|
||||
if (type === "statement")
|
||||
args.push(`async () => {${generator.statementToCode(block, key)}}`);
|
||||
else
|
||||
args.push(
|
||||
generator.valueToCode(block, key, BlocklyJS.Order.NONE) || "null"
|
||||
);
|
||||
}
|
||||
|
||||
return `await MyFunctions[${generator.quote_(block.functionId_)}](${args.join(
|
||||
", "
|
||||
)});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["functions_return"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE);
|
||||
return `return ${value || "null"};\n`;
|
||||
};
|
||||
512
src/blocks/json.js
Normal file
@@ -0,0 +1,512 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
const xmlUtils = Blockly.utils.xml;
|
||||
|
||||
Blockly.Blocks["json_get"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("value of");
|
||||
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
|
||||
this.setOutput(true);
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Returns the value of a key from a JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_get"] = function (block) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.MEMBER
|
||||
) || "{}";
|
||||
const key =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.NONE
|
||||
) || '""';
|
||||
return [`${obj}[${key}]`, BlocklyJS.Order.MEMBER];
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_set"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("set");
|
||||
this.appendValueInput("VALUE").appendField("to");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Sets a value to a key in a JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_set"] = function (block) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.MEMBER
|
||||
) || "{}";
|
||||
const key =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.NONE
|
||||
) || '""';
|
||||
const value =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
BlocklyJS.Order.ASSIGNMENT
|
||||
) || "null";
|
||||
return `${obj}[${key}] = ${value};\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_delete"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("OBJECT").setCheck("Object").appendField("in object");
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("remove");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Deletes a key from a JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_delete"] = function (block) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.MEMBER
|
||||
) || "{}";
|
||||
const key =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.NONE
|
||||
) || '""';
|
||||
return `delete ${obj}[${key}];\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_create_item"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("key and value");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Add a key with a value to the object.");
|
||||
this.contextMenu = false;
|
||||
},
|
||||
};
|
||||
|
||||
/* --- start deprecated --- */
|
||||
Blockly.Blocks["json_key_value"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("key");
|
||||
this.appendValueInput("VALUE").setCheck(null).appendField("value");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("A single key with a value.");
|
||||
this.setOutput(true, "ObjectItem");
|
||||
this.setInputsInline(true);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_key_value"] = function (block) {
|
||||
const keyCode =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.NONE
|
||||
) || '""';
|
||||
const valCode =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "null";
|
||||
const code = `${keyCode}: ${valCode}`;
|
||||
return [code, BlocklyJS.Order.ATOMIC];
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_create"] = {
|
||||
init: function () {
|
||||
this.setOutput(true, "Object");
|
||||
this.setStyle("json_category");
|
||||
this.itemCount_ = 0;
|
||||
this.updateShape_();
|
||||
this.setMutator(new Blockly.icons.MutatorIcon(["json_create_item"], this));
|
||||
this.setTooltip("Create a JSON object with any number of keys.");
|
||||
this.setInputsInline(false);
|
||||
},
|
||||
mutationToDom: function () {
|
||||
const container = xmlUtils.createElement("mutation");
|
||||
container.setAttribute("items", String(this.itemCount_));
|
||||
return container;
|
||||
},
|
||||
domToMutation: function (xmlElement) {
|
||||
const items = xmlElement.getAttribute("items");
|
||||
if (!items) throw new TypeError("element did not have items");
|
||||
this.itemCount_ = parseInt(items, 10);
|
||||
this.updateShape_();
|
||||
},
|
||||
saveExtraState: function () {
|
||||
return {
|
||||
itemCount: this.itemCount_,
|
||||
};
|
||||
},
|
||||
loadExtraState: function (state) {
|
||||
this.itemCount_ = state["itemCount"];
|
||||
this.updateShape_();
|
||||
},
|
||||
saveConnections: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
let i = 0;
|
||||
while (itemBlock) {
|
||||
const input = this.getInput("ITEM" + i);
|
||||
itemBlock.valueConnection_ = input && input.connection.targetConnection;
|
||||
itemBlock = itemBlock.nextConnection?.targetBlock();
|
||||
i++;
|
||||
}
|
||||
},
|
||||
compose: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
const connections = [];
|
||||
while (itemBlock) {
|
||||
if (itemBlock.isInsertionMarker()) {
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
continue;
|
||||
}
|
||||
connections.push(itemBlock.valueConnection_);
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
}
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const connection = this.getInput("ADD" + i)?.connection?.targetConnection;
|
||||
if (connection && !connections.includes(connection)) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
this.itemCount_ = connections.length;
|
||||
this.updateShape_();
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
connections[i]?.reconnect(this, "ADD" + i);
|
||||
}
|
||||
},
|
||||
decompose: function (workspace) {
|
||||
const containerBlock = workspace.newBlock("json_create_container");
|
||||
containerBlock.initSvg();
|
||||
let connection = containerBlock.getInput("STACK").connection;
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const itemBlock = workspace.newBlock("json_create_item");
|
||||
itemBlock.initSvg();
|
||||
if (!itemBlock.previousConnection) {
|
||||
throw new Error("itemBlock has no previousConnection");
|
||||
}
|
||||
connection.connect(itemBlock.previousConnection);
|
||||
connection = itemBlock.nextConnection;
|
||||
}
|
||||
return containerBlock;
|
||||
},
|
||||
saveConnections: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
let i = 0;
|
||||
while (itemBlock) {
|
||||
if (itemBlock.isInsertionMarker()) {
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
continue;
|
||||
}
|
||||
const input = this.getInput("ADD" + i);
|
||||
itemBlock.valueConnection_ = input?.connection?.targetConnection;
|
||||
itemBlock = itemBlock?.getNextBlock();
|
||||
i++;
|
||||
}
|
||||
},
|
||||
updateShape_: function () {
|
||||
if (this.itemCount_ && this.getInput("EMPTY")) {
|
||||
this.removeInput("EMPTY");
|
||||
} else if (!this.itemCount_ && !this.getInput("EMPTY")) {
|
||||
this.appendDummyInput("EMPTY").appendField("create empty object");
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
if (!this.getInput("ADD" + i)) {
|
||||
const input = this.appendValueInput("ADD" + i).setAlign(
|
||||
Blockly.inputs.Align.RIGHT
|
||||
);
|
||||
input.setCheck("ObjectItem");
|
||||
if (i === 0) input.appendField("create object with");
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = this.itemCount_; this.getInput("ADD" + i); i++) {
|
||||
this.removeInput("ADD" + i);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_create_container"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("object");
|
||||
this.appendStatementInput("STACK");
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip(
|
||||
"Add, remove, or reorder sections to configure this object block."
|
||||
);
|
||||
this.contextMenu = false;
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_create"] = function (block) {
|
||||
const entries = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const pairCode = BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"ADD" + i,
|
||||
BlocklyJS.Order.NONE
|
||||
);
|
||||
if (pairCode) {
|
||||
entries.push(pairCode || "");
|
||||
}
|
||||
}
|
||||
const code = `{ ${entries.join(", ")} }`;
|
||||
return [code, BlocklyJS.Order.ATOMIC];
|
||||
};
|
||||
/* --- end deprecated --- */
|
||||
|
||||
Blockly.Blocks["json_create_statement"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("create object");
|
||||
this.appendStatementInput("STACK").setCheck("json_key_value");
|
||||
this.setOutput(true, "Object");
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Create a JSON object using stacked key/value pairs.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_create_statement"] = function (
|
||||
block
|
||||
) {
|
||||
const statements = BlocklyJS.javascriptGenerator.statementToCode(
|
||||
block,
|
||||
"STACK"
|
||||
);
|
||||
return [`{\n${statements}}`, BlocklyJS.Order.ATOMIC];
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_key_value_statement"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("key");
|
||||
this.appendValueInput("VALUE").appendField("value");
|
||||
this.setPreviousStatement(true, "json_key_value");
|
||||
this.setNextStatement(true, "json_key_value");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("A single key/value pair for a JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_key_value_statement"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const key =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "";
|
||||
const value =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "null";
|
||||
|
||||
if (key) return `${key}: ${value},\n`;
|
||||
else return "";
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_has_key"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("OBJECT")
|
||||
.setCheck("Object")
|
||||
.appendField("does object");
|
||||
this.appendValueInput("KEY").setCheck("String").appendField("have");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Returns true if the key exists in the object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_has_key"] = function (block) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.MEMBER
|
||||
) || "{}";
|
||||
const key =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"KEY",
|
||||
BlocklyJS.Order.NONE
|
||||
) || '""';
|
||||
return [`(${key} in ${obj})`, BlocklyJS.Order.RELATIONAL];
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_property_list"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("OBJECT")
|
||||
.setCheck("Object")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["keys", "KEYS"],
|
||||
["values", "VALUES"],
|
||||
["entries", "ENTRIES"],
|
||||
]),
|
||||
"MODE"
|
||||
)
|
||||
.appendField("of");
|
||||
this.setOutput(true, "Array");
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Gets keys, values, or entries of the JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_property_list"] = function (
|
||||
block
|
||||
) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.FUNCTION_CALL
|
||||
) || "{}";
|
||||
const mode = block.getFieldValue("MODE");
|
||||
|
||||
let code;
|
||||
switch (mode) {
|
||||
case "KEYS":
|
||||
code = `Object.keys(${obj})`;
|
||||
break;
|
||||
case "VALUES":
|
||||
code = `Object.values(${obj})`;
|
||||
break;
|
||||
case "ENTRIES":
|
||||
code = `Object.entries(${obj})`;
|
||||
break;
|
||||
default:
|
||||
code = "[]";
|
||||
}
|
||||
|
||||
return [code, BlocklyJS.Order.FUNCTION_CALL];
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_parse"] = {
|
||||
init: function () {
|
||||
const dropdown = new Blockly.FieldDropdown([
|
||||
["object from text", "PARSE"],
|
||||
["text from object", "STRINGIFY"],
|
||||
]);
|
||||
dropdown.setValidator((newMode) => {
|
||||
this.updateType_(newMode);
|
||||
});
|
||||
|
||||
this.setStyle("json_category");
|
||||
this.appendValueInput("INPUT")
|
||||
.setCheck("String")
|
||||
.appendField("make")
|
||||
.appendField(dropdown, "MODE");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, "Object");
|
||||
this.setTooltip(() => {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
if (mode === "PARSE") {
|
||||
return "Convert a stringified object into an object.";
|
||||
} else if (mode === "STRINGIFY") {
|
||||
return "Convert an object into text representing an object.";
|
||||
}
|
||||
throw Error("Unknown mode: " + mode);
|
||||
});
|
||||
},
|
||||
updateType_: function (newMode) {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
if (mode !== newMode) {
|
||||
const inputConnection = this.getInput("INPUT")?.connection;
|
||||
inputConnection?.setShadowDom(null);
|
||||
const inputBlock = inputConnection?.targetBlock();
|
||||
|
||||
if (inputBlock) {
|
||||
inputConnection.disconnect();
|
||||
if (inputBlock.isShadow()) {
|
||||
inputBlock.dispose(false);
|
||||
} else {
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMode === "PARSE") {
|
||||
this.outputConnection.setCheck("Object");
|
||||
this.getInput("INPUT").setCheck("String");
|
||||
} else {
|
||||
this.outputConnection.setCheck("String");
|
||||
this.getInput("INPUT").setCheck("Object");
|
||||
}
|
||||
},
|
||||
mutationToDom: function () {
|
||||
const container = xmlUtils.createElement("mutation");
|
||||
container.setAttribute("mode", this.getFieldValue("MODE"));
|
||||
return container;
|
||||
},
|
||||
domToMutation: function (xmlElement) {
|
||||
this.updateType_(xmlElement.getAttribute("mode"));
|
||||
},
|
||||
saveExtraState: function () {
|
||||
return { mode: this.getFieldValue("MODE") };
|
||||
},
|
||||
loadExtraState: function (state) {
|
||||
this.updateType_(state["mode"]);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_parse"] = function (block) {
|
||||
const input =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"INPUT",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "null";
|
||||
const mode = block.getFieldValue("MODE");
|
||||
|
||||
if (mode === "PARSE") {
|
||||
return [`JSON.parse(${input})`, BlocklyJS.Order.NONE];
|
||||
} else if (mode === "STRINGIFY") {
|
||||
return [`JSON.stringify(${input})`, BlocklyJS.Order.NONE];
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks["json_clone"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("OBJECT")
|
||||
.setCheck("Object")
|
||||
.appendField("clone object");
|
||||
this.setOutput(true, "Object");
|
||||
this.setStyle("json_category");
|
||||
this.setTooltip("Creates a duplicate of a JSON object.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["json_clone"] = function (block) {
|
||||
const obj =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"OBJECT",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "{}";
|
||||
return [`JSON.parse(JSON.stringify(${obj}))`, BlocklyJS.Order.FUNCTION_CALL];
|
||||
};
|
||||
454
src/blocks/list.js
Normal file
@@ -0,0 +1,454 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["lists_filter"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("list").setCheck("Array").appendField("filter list");
|
||||
this.appendValueInput("method").setCheck("Boolean").appendField("by");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, "Array");
|
||||
this.setStyle("list_blocks");
|
||||
this.setTooltip(
|
||||
"Remove all items in a list which doesn't match the boolean"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_filter"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
|
||||
var val_method = generator.valueToCode(
|
||||
block,
|
||||
"method",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
);
|
||||
var code = `${val_list}.filter(findOrFilterItem => ${val_method})`;
|
||||
return [code, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["lists_find"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("list").setCheck("Array").appendField("in list");
|
||||
this.appendValueInput("method")
|
||||
.setCheck("Boolean")
|
||||
.appendField("find first that matches");
|
||||
this.setOutput(true, null);
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("list_blocks");
|
||||
this.setTooltip(
|
||||
"Returns the first item in a list that matches the boolean"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_find"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
var val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
|
||||
var val_method = generator.valueToCode(
|
||||
block,
|
||||
"method",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
);
|
||||
var code = `${val_list}.find(findOrFilterItem => ${val_method})`;
|
||||
return [code, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["lists_filter_item"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput("name").appendField("item in loop");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, null);
|
||||
this.setStyle("list_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_filter_item"] = () => [
|
||||
"findOrFilterItem",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
Blockly.Blocks["lists_merge"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("list").setCheck("Array").appendField("merge list");
|
||||
this.appendValueInput("list2").setCheck("Array").appendField("with");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, "Array");
|
||||
this.setStyle("list_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_merge"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const val_list = generator.valueToCode(block, "list", BlocklyJS.Order.ATOMIC);
|
||||
const val_list2 = generator.valueToCode(
|
||||
block,
|
||||
"list2",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
);
|
||||
const code = `${val_list}.concat(${val_list2})`;
|
||||
return [code, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["lists_foreach"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("LIST")
|
||||
.setCheck("Array")
|
||||
.appendField("for each item in list");
|
||||
this.appendStatementInput("DO").appendField("do");
|
||||
this.setInputsInline(false);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setStyle("list_blocks");
|
||||
this.setTooltip(
|
||||
"Loops through every item in a list and runs the code inside for each one"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_foreach"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const list =
|
||||
generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]";
|
||||
const branch = generator.statementToCode(block, "DO");
|
||||
const code = `${list}.forEach(findOrFilterItem => {\n${branch}});\n`;
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.Blocks["lists_getIndex_modified"] = {
|
||||
init: function () {
|
||||
const MODE_OPTIONS = [
|
||||
[Blockly.Msg.LISTS_GET_INDEX_GET, "GET"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_REMOVE, "REMOVE"],
|
||||
];
|
||||
this.WHERE_OPTIONS = [
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"],
|
||||
];
|
||||
|
||||
this.setHelpUrl(Blockly.Msg.LISTS_GET_INDEX_HELPURL);
|
||||
this.setStyle("list_blocks");
|
||||
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck("Array")
|
||||
.appendField(Blockly.Msg.LISTS_GET_INDEX_INPUT_IN_LIST);
|
||||
|
||||
const modeField = new Blockly.FieldDropdown(MODE_OPTIONS, newMode => {
|
||||
this.updateMode_(newMode);
|
||||
return newMode;
|
||||
});
|
||||
this.appendDummyInput()
|
||||
.appendField(modeField, "MODE")
|
||||
.appendField("", "SPACE");
|
||||
|
||||
const whereField = new Blockly.FieldDropdown(
|
||||
this.WHERE_OPTIONS,
|
||||
newWhere => {
|
||||
const cur = this.getFieldValue("WHERE");
|
||||
const newNeedsAt = newWhere === "FROM_START" || newWhere === "FROM_END";
|
||||
const curNeedsAt = cur === "FROM_START" || cur === "FROM_END";
|
||||
if (newNeedsAt !== curNeedsAt) this.updateAt_(newNeedsAt);
|
||||
}
|
||||
);
|
||||
this.appendDummyInput().appendField(whereField, "WHERE");
|
||||
|
||||
this.appendDummyInput("AT");
|
||||
|
||||
this.setInputsInline(true);
|
||||
|
||||
this.updateAt_(true);
|
||||
this.updateMode_(this.getFieldValue("MODE") || "GET");
|
||||
|
||||
this.setTooltip(() => {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
const where = this.getFieldValue("WHERE");
|
||||
if (mode === "GET") {
|
||||
switch (where) {
|
||||
case "FROM_START":
|
||||
case "FROM_END":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FROM;
|
||||
case "FIRST":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_FIRST;
|
||||
case "LAST":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_LAST;
|
||||
case "RANDOM":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_GET_RANDOM;
|
||||
}
|
||||
} else {
|
||||
switch (where) {
|
||||
case "FROM_START":
|
||||
case "FROM_END":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FROM;
|
||||
case "FIRST":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_FIRST;
|
||||
case "LAST":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_LAST;
|
||||
case "RANDOM":
|
||||
return Blockly.Msg.LISTS_GET_INDEX_TOOLTIP_REMOVE_RANDOM;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = document.createElement("mutation");
|
||||
const input = this.getInput("AT");
|
||||
const isValueInput = !!input && !!input.connection;
|
||||
container.setAttribute("at", String(isValueInput));
|
||||
container.setAttribute("mode", this.getFieldValue("MODE") || "GET");
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const at = xmlElement.getAttribute("at") !== "false";
|
||||
const mode = xmlElement.getAttribute("mode") || "GET";
|
||||
this.updateAt_(at);
|
||||
this.updateMode_(mode);
|
||||
},
|
||||
|
||||
updateAt_: function (useValueInput) {
|
||||
if (this.getInput("AT")) this.removeInput("AT", true);
|
||||
if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true);
|
||||
|
||||
if (useValueInput) {
|
||||
this.appendValueInput("AT").setCheck("Number");
|
||||
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
|
||||
this.appendDummyInput("ORDINAL").appendField(
|
||||
Blockly.Msg.ORDINAL_NUMBER_SUFFIX
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.appendDummyInput("AT");
|
||||
}
|
||||
},
|
||||
|
||||
updateMode_: function (mode) {
|
||||
if (mode === "GET") {
|
||||
if (!this.outputConnection) this.setOutput(true);
|
||||
this.outputConnection.setCheck(null);
|
||||
} else {
|
||||
if (!this.outputConnection) this.setOutput(true);
|
||||
this.outputConnection.setCheck("Array");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_getIndex_modified"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const workspaceOneBased =
|
||||
block.workspace.options && block.workspace.options.oneBasedIndex;
|
||||
const mode = block.getFieldValue("MODE");
|
||||
const where = block.getFieldValue("WHERE");
|
||||
|
||||
const listCode =
|
||||
generator.valueToCode(block, "VALUE", BlocklyJS.Order.NONE) || "[]";
|
||||
const atCodeRaw =
|
||||
generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0";
|
||||
|
||||
let i;
|
||||
|
||||
switch (where) {
|
||||
case "FIRST":
|
||||
i = "0";
|
||||
break;
|
||||
case "LAST":
|
||||
i = "-1";
|
||||
break;
|
||||
case "RANDOM":
|
||||
i = `Math.floor(Math.random() * _.length)`;
|
||||
break;
|
||||
case "FROM_START":
|
||||
i = workspaceOneBased ? `${atCodeRaw} - 1` : atCodeRaw;
|
||||
break;
|
||||
case "FROM_END":
|
||||
i = workspaceOneBased ? `-${atCodeRaw}` : `-(${atCodeRaw} + 1)`;
|
||||
break;
|
||||
default:
|
||||
i = "0";
|
||||
}
|
||||
|
||||
return [
|
||||
mode === "REMOVE"
|
||||
? `${listCode}.toSpliced(${i}, 1)`
|
||||
: `${listCode}.at(${i})`,
|
||||
BlocklyJS.Order.FUNCTION_CALL,
|
||||
];
|
||||
};
|
||||
|
||||
Blockly.Blocks["lists_setIndex_modified"] = {
|
||||
init: function () {
|
||||
const MODE_OPTIONS = [
|
||||
[Blockly.Msg.LISTS_SET_INDEX_SET, "SET"],
|
||||
[Blockly.Msg.LISTS_SET_INDEX_INSERT, "INSERT"],
|
||||
];
|
||||
this.WHERE_OPTIONS = [
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FROM_START, "FROM_START"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FROM_END, "FROM_END"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_FIRST, "FIRST"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_LAST, "LAST"],
|
||||
[Blockly.Msg.LISTS_GET_INDEX_RANDOM, "RANDOM"],
|
||||
];
|
||||
|
||||
this.setHelpUrl(Blockly.Msg.LISTS_SET_INDEX_HELPURL);
|
||||
this.setStyle("list_blocks");
|
||||
|
||||
this.appendValueInput("LIST")
|
||||
.setCheck("Array")
|
||||
.appendField(Blockly.Msg.LISTS_SET_INDEX_INPUT_IN_LIST);
|
||||
|
||||
const modeField = new Blockly.FieldDropdown(MODE_OPTIONS);
|
||||
this.appendDummyInput()
|
||||
.appendField(modeField, "MODE")
|
||||
.appendField("", "SPACE");
|
||||
|
||||
const whereField = new Blockly.FieldDropdown(
|
||||
this.WHERE_OPTIONS,
|
||||
newWhere => {
|
||||
this.updateAt_(newWhere === "FROM_START" || newWhere === "FROM_END");
|
||||
return newWhere;
|
||||
}
|
||||
);
|
||||
this.appendDummyInput().appendField(whereField, "WHERE");
|
||||
|
||||
this.appendDummyInput("AT");
|
||||
|
||||
this.appendValueInput("TO").appendField(
|
||||
Blockly.Msg.LISTS_SET_INDEX_INPUT_TO
|
||||
);
|
||||
|
||||
this.setInputsInline(true);
|
||||
|
||||
this.setOutput(true, "Array");
|
||||
|
||||
this.updateAt_(true);
|
||||
|
||||
this.setTooltip(() => {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
const where = this.getFieldValue("WHERE");
|
||||
if (mode === "SET") {
|
||||
switch (where) {
|
||||
case "FROM_START":
|
||||
case "FROM_END":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FROM;
|
||||
case "FIRST":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_FIRST;
|
||||
case "LAST":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_LAST;
|
||||
case "RANDOM":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_SET_RANDOM;
|
||||
}
|
||||
} else {
|
||||
switch (where) {
|
||||
case "FROM_START":
|
||||
case "FROM_END":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FROM;
|
||||
case "FIRST":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_FIRST;
|
||||
case "LAST":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_LAST;
|
||||
case "RANDOM":
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP_INSERT_RANDOM;
|
||||
}
|
||||
}
|
||||
return Blockly.Msg.LISTS_SET_INDEX_TOOLTIP;
|
||||
});
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = document.createElement("mutation");
|
||||
const input = this.getInput("AT");
|
||||
const isValueInput = !!input && !!input.connection;
|
||||
container.setAttribute("at", String(isValueInput));
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const at = xmlElement.getAttribute("at") !== "false";
|
||||
this.updateAt_(at);
|
||||
},
|
||||
|
||||
updateAt_: function (useValueInput) {
|
||||
if (this.getInput("AT")) this.removeInput("AT", true);
|
||||
if (this.getInput("ORDINAL")) this.removeInput("ORDINAL", true);
|
||||
|
||||
if (useValueInput) {
|
||||
this.appendValueInput("AT").setCheck("Number");
|
||||
if (Blockly.Msg.ORDINAL_NUMBER_SUFFIX) {
|
||||
this.appendDummyInput("ORDINAL").appendField(
|
||||
Blockly.Msg.ORDINAL_NUMBER_SUFFIX
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.appendDummyInput("AT");
|
||||
}
|
||||
try {
|
||||
this.moveInputBefore("AT", "TO");
|
||||
if (this.getInput("ORDINAL")) this.moveInputBefore("ORDINAL", "TO");
|
||||
} catch (e) {}
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["lists_setIndex_modified"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const oneBased = block.workspace.options?.oneBasedIndex;
|
||||
const mode = block.getFieldValue("MODE");
|
||||
const where = block.getFieldValue("WHERE");
|
||||
|
||||
const listCode =
|
||||
generator.valueToCode(block, "LIST", BlocklyJS.Order.NONE) || "[]";
|
||||
const valueCode =
|
||||
generator.valueToCode(block, "TO", BlocklyJS.Order.NONE) || "undefined";
|
||||
const atCode =
|
||||
generator.valueToCode(block, "AT", BlocklyJS.Order.NONE) || "0";
|
||||
|
||||
let indexExpr;
|
||||
switch (where) {
|
||||
case "FIRST":
|
||||
indexExpr = "0";
|
||||
break;
|
||||
case "LAST":
|
||||
indexExpr = "-1";
|
||||
break;
|
||||
case "RANDOM":
|
||||
indexExpr = "Math.floor(Math.random() * _.length)";
|
||||
break;
|
||||
case "FROM_START":
|
||||
indexExpr = oneBased ? `(${atCode} - 1)` : atCode;
|
||||
break;
|
||||
case "FROM_END":
|
||||
indexExpr = oneBased ? `-${atCode}` : `-(${atCode} + 1)`;
|
||||
break;
|
||||
default:
|
||||
indexExpr = "0";
|
||||
}
|
||||
|
||||
const code = `((a) => {
|
||||
const _ = [...a];
|
||||
let i = ${indexExpr};
|
||||
if (i < 0) i += _.length;
|
||||
if (i < 0) i = 0;
|
||||
if (i > _.length) i = _.length;
|
||||
return ${
|
||||
mode === "INSERT"
|
||||
? `_.toSpliced(i, 0, ${valueCode})`
|
||||
: `i < _.length ? _.toSpliced(i, 1, ${valueCode}) : _`
|
||||
};
|
||||
})(${listCode})`;
|
||||
|
||||
return [code, BlocklyJS.Order.FUNCTION_CALL];
|
||||
};
|
||||
218
src/blocks/looks.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
function getAvailableCostumes() {
|
||||
if (window.projectCostumes && window.projectCostumes.length > 0) {
|
||||
return window.projectCostumes.map(costume => [costume, costume]);
|
||||
}
|
||||
return [["default", "default"]];
|
||||
}
|
||||
|
||||
Blockly.Blocks["say_message"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("MESSAGE").appendField("say");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["say_message_duration"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("MESSAGE").appendField("say");
|
||||
this.appendValueInput("DURATION").setCheck("Number").appendField("for");
|
||||
this.appendDummyInput().appendField("seconds");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["switch_costume"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("switch costume to")
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
function() {
|
||||
return getAvailableCostumes();
|
||||
}
|
||||
), "COSTUME_NAME");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_size"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("set size to");
|
||||
this.appendDummyInput().appendField("%");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["change_size"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("change size by");
|
||||
this.appendDummyInput().appendField("%");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_costume_size"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("costume")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["width", "width"],
|
||||
["height", "height"],
|
||||
]),
|
||||
"MENU"
|
||||
);
|
||||
this.setOutput(true, "Number");
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_sprite_scale"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("size");
|
||||
this.setOutput(true, "Number");
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["looks_hide_sprite"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("hide sprite");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["looks_show_sprite"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("show sprite");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["looks_isVisible"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("is visible");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["say_message"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const message =
|
||||
generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || "";
|
||||
|
||||
return `sayMessage(${message});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["say_message_duration"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const message =
|
||||
generator.valueToCode(block, "MESSAGE", BlocklyJS.Order.NONE) || "";
|
||||
const duration =
|
||||
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) || 2;
|
||||
|
||||
return `sayMessage(${message}, ${duration});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
var costume = generator.valueToCode(block, "COSTUME", BlocklyJS.Order.ATOMIC);
|
||||
return `switchCostume(${costume});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["switch_costume"] = function (block, generator) {
|
||||
var costume = "'" + block.getFieldValue("COSTUME_NAME") + "'";
|
||||
return `switchCostume(${costume});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_size"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100;
|
||||
return `setSize(${amount}, false);\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["change_size"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 100;
|
||||
return `setSize(${amount}, true);\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_costume_size"] = function (block) {
|
||||
const menu = block.getFieldValue("MENU");
|
||||
return [`getCostumeSize("${menu}")`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_sprite_scale"] = function () {
|
||||
return [`getSpriteScale()`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["looks_hide_sprite"] = function () {
|
||||
return "toggleVisibility(false);\n";
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["looks_show_sprite"] = function () {
|
||||
return "toggleVisibility(true);\n";
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["looks_isVisible"] = () => [
|
||||
"sprite.visible",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
Blockly.Blocks["switch_backdrop"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("switch backdrop to")
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
function() {
|
||||
if (window.projectBackdrops && window.projectBackdrops.length > 0) {
|
||||
return window.projectBackdrops.map((backdrop) => [backdrop.name, backdrop.name]);
|
||||
}
|
||||
return [["no backdrops", ""]];
|
||||
}
|
||||
), "BACKDROP_NAME");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.setStyle("looks_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["switch_backdrop"] = function (block) {
|
||||
const name = block.getFieldValue("BACKDROP_NAME");
|
||||
return `setBackdropByName('${name}');\n`;
|
||||
};
|
||||
223
src/blocks/motion.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["move_steps"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("STEPS").setCheck("Number").appendField("step");
|
||||
this.appendDummyInput().appendField("times");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["change_position"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("change")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["x", "x"],
|
||||
["y", "y"],
|
||||
]),
|
||||
"MENU"
|
||||
)
|
||||
.appendField("by");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_position"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("set")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["x", "x"],
|
||||
["y", "y"],
|
||||
]),
|
||||
"MENU"
|
||||
)
|
||||
.appendField("to");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["goto_position"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("x").setCheck("Number").appendField("go to x");
|
||||
this.appendValueInput("y").setCheck("Number").appendField("y");
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_position"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["x", "x"],
|
||||
["y", "y"],
|
||||
]),
|
||||
"MENU"
|
||||
);
|
||||
this.setOutput(true, "Number");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["angle_turn"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("turn")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
[
|
||||
{
|
||||
src: "icons/right.svg",
|
||||
height: 30,
|
||||
width: 30,
|
||||
alt: "A circular arrow rotating to the right",
|
||||
},
|
||||
"right",
|
||||
],
|
||||
[
|
||||
{
|
||||
src: "icons/left.svg",
|
||||
height: 30,
|
||||
width: 30,
|
||||
alt: "A circular arrow rotating to the left",
|
||||
},
|
||||
"left",
|
||||
],
|
||||
]),
|
||||
"DIRECTION"
|
||||
);
|
||||
this.appendDummyInput().appendField("degrees");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["angle_set"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("AMOUNT")
|
||||
.setCheck("Number")
|
||||
.appendField("set angle to");
|
||||
this.appendDummyInput().appendField("degrees");
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["point_towards"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("x")
|
||||
.setCheck("Number")
|
||||
.appendField("point towards x");
|
||||
this.appendValueInput("y").setCheck("Number").appendField("y");
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_angle"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("angle");
|
||||
this.setOutput(true, "Number");
|
||||
this.setStyle("motion_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["move_steps"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const steps =
|
||||
generator.valueToCode(block, "STEPS", BlocklyJS.Order.ATOMIC) || 0;
|
||||
return `moveSteps(${steps});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["change_position"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const menu = block.getFieldValue("MENU");
|
||||
if (menu === "y") return `sprite["${menu}"] -= ${amount};\n`;
|
||||
else return `sprite["${menu}"] += ${amount};\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_position"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const menu = block.getFieldValue("MENU");
|
||||
if (menu === "y") return `sprite["${menu}"] = -${amount};\n`;
|
||||
else return `sprite["${menu}"] = ${amount};\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["goto_position"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0;
|
||||
return `sprite.x = ${x};\nsprite.y = -${y};\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["point_towards"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const x = generator.valueToCode(block, "x", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const y = generator.valueToCode(block, "y", BlocklyJS.Order.ATOMIC) || 0;
|
||||
return `pointsTowards(${x}, ${y});\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_position"] = function (block) {
|
||||
const menu = block.getFieldValue("MENU");
|
||||
return [`sprite["${menu}"]`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["angle_turn"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const direction = block.getFieldValue("DIRECTION");
|
||||
let amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
|
||||
if (direction === "left") amount = `-(${amount})`;
|
||||
return `setAngle(${amount}, true);\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["angle_set"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const amount =
|
||||
generator.valueToCode(block, "AMOUNT", BlocklyJS.Order.ATOMIC) || 0;
|
||||
return `setAngle(${amount}, false);\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_angle"] = () => [
|
||||
"sprite.angle",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
115
src/blocks/pen.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["pen_down"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("pen down");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Put the pen down to draw");
|
||||
},
|
||||
};
|
||||
BlocklyJS.javascriptGenerator.forBlock["pen_down"] = function () {
|
||||
return "setPenStatus(true);\n";
|
||||
};
|
||||
|
||||
Blockly.Blocks["pen_up"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("pen up");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Lift the pen up");
|
||||
},
|
||||
};
|
||||
BlocklyJS.javascriptGenerator.forBlock["pen_up"] = function () {
|
||||
return "setPenStatus(false);\n";
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_pen_color"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("set pen color");
|
||||
this.appendValueInput("R").setCheck("Number").appendField("R");
|
||||
this.appendValueInput("G").setCheck("Number").appendField("G");
|
||||
this.appendValueInput("B").setCheck("Number").appendField("B");
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Set the pen color to a RGB value");
|
||||
},
|
||||
};
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_pen_color"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const r = generator.valueToCode(block, "R", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const g = generator.valueToCode(block, "G", BlocklyJS.Order.ATOMIC) || 0;
|
||||
const b = generator.valueToCode(block, "B", BlocklyJS.Order.ATOMIC) || 0;
|
||||
return `setPenColor(${r}, ${g}, ${b});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_pen_color_combined"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput("MODE")
|
||||
.appendField("set pen color to")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["RGB", "RGB"],
|
||||
["HEX", "HEX"],
|
||||
]),
|
||||
"MODE"
|
||||
);
|
||||
this.appendValueInput("VALUE").setCheck(["String", "Number"]);
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Set the pen color to a RGB or HEX value.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_pen_color_combined"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const mode = block.getFieldValue("MODE");
|
||||
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
|
||||
if (mode === "HEX") return `setPenColorHex(${value});\n`;
|
||||
else return `setPenColor(${value});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_pen_size"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SIZE")
|
||||
.setCheck("Number")
|
||||
.appendField("set pen size to");
|
||||
this.appendDummyInput().appendField("px");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Set the pen thickness to a specific value in pixels");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_pen_size"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const size =
|
||||
generator.valueToCode(block, "SIZE", BlocklyJS.Order.ATOMIC) || 1;
|
||||
return `setPenSize("${size}");\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["clear_pen"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("clear pen");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#0fbd8c");
|
||||
this.setTooltip("Clear all pen drawings");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["clear_pen"] = () => "clearPen();\n";
|
||||
353
src/blocks/set.js
Normal file
@@ -0,0 +1,353 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
Blockly.Blocks["sets_create_with"] = {
|
||||
init: function () {
|
||||
this.setStyle("set_blocks");
|
||||
this.setHelpUrl("");
|
||||
this.itemCount_ = 0;
|
||||
this.updateShape_();
|
||||
this.setOutput(true, "Set");
|
||||
this.setMutator(
|
||||
new Blockly.icons.MutatorIcon(["sets_create_with_item"], this)
|
||||
);
|
||||
this.setTooltip("Create a set with any number of elements.");
|
||||
},
|
||||
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("items", String(this.itemCount_));
|
||||
return container;
|
||||
},
|
||||
|
||||
domToMutation: function (xmlElement) {
|
||||
const items = xmlElement.getAttribute("items");
|
||||
this.itemCount_ = items ? parseInt(items, 10) : 0;
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
saveExtraState: function () {
|
||||
return { itemCount: this.itemCount_ };
|
||||
},
|
||||
|
||||
loadExtraState: function (state) {
|
||||
if (state && typeof state.itemCount === "number") {
|
||||
this.itemCount_ = state.itemCount;
|
||||
} else {
|
||||
this.itemCount_ = 0;
|
||||
}
|
||||
this.updateShape_();
|
||||
},
|
||||
|
||||
decompose: function (workspace) {
|
||||
const containerBlock = workspace.newBlock("sets_create_with_container");
|
||||
containerBlock.initSvg();
|
||||
let connection = containerBlock.getInput("STACK").connection;
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const itemBlock = workspace.newBlock("sets_create_with_item");
|
||||
itemBlock.initSvg();
|
||||
connection.connect(itemBlock.previousConnection);
|
||||
connection = itemBlock.nextConnection;
|
||||
}
|
||||
return containerBlock;
|
||||
},
|
||||
|
||||
compose: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
const connections = [];
|
||||
while (itemBlock) {
|
||||
if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) {
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
continue;
|
||||
}
|
||||
connections.push(itemBlock.valueConnection_ || null);
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
const input = this.getInput("ADD" + i);
|
||||
const targetConnection =
|
||||
input && input.connection && input.connection.targetConnection;
|
||||
if (targetConnection && !connections.includes(targetConnection)) {
|
||||
targetConnection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
this.itemCount_ = connections.length;
|
||||
this.updateShape_();
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
if (connections[i]) {
|
||||
connections[i].reconnect(this, "ADD" + i);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
saveConnections: function (containerBlock) {
|
||||
let itemBlock = containerBlock.getInputTargetBlock("STACK");
|
||||
let i = 0;
|
||||
while (itemBlock) {
|
||||
if (itemBlock.isInsertionMarker && itemBlock.isInsertionMarker()) {
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
continue;
|
||||
}
|
||||
const input = this.getInput("ADD" + i);
|
||||
itemBlock.valueConnection_ =
|
||||
input && input.connection && input.connection.targetConnection;
|
||||
itemBlock = itemBlock.getNextBlock();
|
||||
i++;
|
||||
}
|
||||
},
|
||||
|
||||
updateShape_: function () {
|
||||
if (this.itemCount_ === 0) {
|
||||
if (!this.getInput("EMPTY")) {
|
||||
this.appendDummyInput("EMPTY").appendField("create empty set");
|
||||
}
|
||||
} else {
|
||||
if (this.getInput("EMPTY")) {
|
||||
this.removeInput("EMPTY");
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.itemCount_; i++) {
|
||||
if (!this.getInput("ADD" + i)) {
|
||||
const input = this.appendValueInput("ADD" + i).setAlign(
|
||||
Blockly.inputs.Align.RIGHT
|
||||
);
|
||||
if (i === 0) {
|
||||
input.appendField("create set with");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let i = this.itemCount_;
|
||||
while (this.getInput("ADD" + i)) {
|
||||
this.removeInput("ADD" + i);
|
||||
i++;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_create_with_container"] = {
|
||||
init: function () {
|
||||
this.setStyle("set_blocks");
|
||||
this.appendDummyInput().appendField("set");
|
||||
this.appendStatementInput("STACK");
|
||||
this.contextMenu = false;
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_create_with_item"] = {
|
||||
init: function () {
|
||||
this.setStyle("set_blocks");
|
||||
this.appendDummyInput().appendField("element");
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(true);
|
||||
this.contextMenu = false;
|
||||
/** @type {Blockly.Connection?} */
|
||||
this.valueConnection_ = null;
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_create_with"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const elements = [];
|
||||
for (let i = 0; i < block.itemCount_; i++) {
|
||||
const code =
|
||||
generator.valueToCode(block, "ADD" + i, BlocklyJS.Order.NONE) || "null";
|
||||
elements.push(code);
|
||||
}
|
||||
let code;
|
||||
if (elements.length === 0) {
|
||||
code = "new Set()";
|
||||
} else {
|
||||
code = "new Set([" + elements.join(", ") + "])";
|
||||
}
|
||||
return [code, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_convert"] = {
|
||||
init: function () {
|
||||
const dropdown = new Blockly.FieldDropdown([
|
||||
["set from list", "SET"],
|
||||
["list from set", "LIST"],
|
||||
]);
|
||||
dropdown.setValidator((newMode) => {
|
||||
this.updateType_(newMode);
|
||||
});
|
||||
|
||||
this.setStyle("set_blocks");
|
||||
this.appendValueInput("INPUT")
|
||||
.setCheck("String")
|
||||
.appendField("make")
|
||||
.appendField(dropdown, "MODE");
|
||||
this.setInputsInline(true);
|
||||
this.setOutput(true, "Set");
|
||||
this.setTooltip(() => {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
if (mode === "SET") {
|
||||
return "Convert a list into a set (removes duplicates).";
|
||||
} else if (mode === "LIST") {
|
||||
return "Convert a set into a list.";
|
||||
}
|
||||
throw Error("Unknown mode: " + mode);
|
||||
});
|
||||
},
|
||||
updateType_: function (newMode) {
|
||||
const mode = this.getFieldValue("MODE");
|
||||
if (mode !== newMode) {
|
||||
const inputConnection = this.getInput("INPUT")?.connection;
|
||||
inputConnection?.setShadowDom(null);
|
||||
const inputBlock = inputConnection?.targetBlock();
|
||||
|
||||
if (inputBlock) {
|
||||
inputConnection.disconnect();
|
||||
if (inputBlock.isShadow()) {
|
||||
inputBlock.dispose(false);
|
||||
} else {
|
||||
this.bumpNeighbours();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMode === "SET") {
|
||||
this.outputConnection.setCheck("Set");
|
||||
this.getInput("INPUT").setCheck("Array");
|
||||
} else {
|
||||
this.outputConnection.setCheck("Array");
|
||||
this.getInput("INPUT").setCheck("Set");
|
||||
}
|
||||
},
|
||||
mutationToDom: function () {
|
||||
const container = Blockly.utils.xml.createElement("mutation");
|
||||
container.setAttribute("mode", this.getFieldValue("MODE"));
|
||||
return container;
|
||||
},
|
||||
domToMutation: function (xmlElement) {
|
||||
this.updateType_(xmlElement.getAttribute("mode"));
|
||||
},
|
||||
saveExtraState: function () {
|
||||
return { mode: this.getFieldValue("MODE") };
|
||||
},
|
||||
loadExtraState: function (state) {
|
||||
this.updateType_(state["mode"]);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_convert"] = function (block) {
|
||||
const input =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"INPUT",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "null";
|
||||
const mode = block.getFieldValue("MODE");
|
||||
|
||||
if (mode === "SET") {
|
||||
return [`new Set(${input})`, BlocklyJS.Order.NONE];
|
||||
} else if (mode === "LIST") {
|
||||
return [`[...${input}]`, BlocklyJS.Order.NONE];
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_add"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SET").setCheck("Set").appendField("in set");
|
||||
this.appendValueInput("VALUE").setCheck(null).appendField("add");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("set_blocks");
|
||||
this.setTooltip("Adds a value to the set.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_add"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
|
||||
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
|
||||
return `${set}.add(${value});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_delete"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SET").setCheck("Set").appendField("in set");
|
||||
this.appendValueInput("VALUE").setCheck(null).appendField("delete");
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("set_blocks");
|
||||
this.setTooltip("Deletes a value from the set.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_delete"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
|
||||
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
|
||||
return `${set}.delete(${value});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_has"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SET").setCheck("Set").appendField("does set");
|
||||
this.appendValueInput("VALUE").setCheck(null).appendField("have");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("set_blocks");
|
||||
this.setTooltip("Returns true if the set contains the value.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_has"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
|
||||
const value = generator.valueToCode(block, "VALUE", BlocklyJS.Order.ATOMIC);
|
||||
return [`${set}.has(${value})`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_size"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SET").setCheck("Set").appendField("size of set");
|
||||
this.setOutput(true, "Number");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("set_blocks");
|
||||
this.setTooltip("Returns how many items are in the set.");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_size"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const set = generator.valueToCode(block, "SET", BlocklyJS.Order.ATOMIC);
|
||||
return [`${set}.size`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
Blockly.Blocks["sets_merge"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("SET1").setCheck("Set").appendField("merge set");
|
||||
this.appendValueInput("SET2").setCheck("Set").appendField("with");
|
||||
this.setOutput(true, "Set");
|
||||
this.setInputsInline(true);
|
||||
this.setStyle("set_blocks");
|
||||
this.setTooltip("Creates a new set combining all values from two sets");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["sets_merge"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const set1 = generator.valueToCode(block, "SET1", BlocklyJS.Order.ATOMIC);
|
||||
const set2 = generator.valueToCode(block, "SET2", BlocklyJS.Order.ATOMIC);
|
||||
return [`new Set([...${set1}, ...${set2}])`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
137
src/blocks/sound.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
// Function to get available sounds - you'll need to implement this in your main editor
|
||||
// This should return an array of sound names from your project
|
||||
function getAvailableSounds() {
|
||||
// This needs to be connected to your sound list
|
||||
// For now, returning a default option
|
||||
if (window.projectSounds && window.projectSounds.length > 0) {
|
||||
return window.projectSounds.map(sound => [sound, sound]);
|
||||
}
|
||||
return [["no sounds", ""]];
|
||||
}
|
||||
|
||||
Blockly.Blocks["play_sound"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("play sound")
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
function() {
|
||||
return getAvailableSounds();
|
||||
}
|
||||
), "SOUND_NAME");
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["until finished", "true"],
|
||||
["without waiting", "false"],
|
||||
]),
|
||||
"wait"
|
||||
);
|
||||
this.setColour("#ff66ba");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["play_sound"] = function (block, generator) {
|
||||
var name = "'" + block.getFieldValue("SOUND_NAME") + "'";
|
||||
var wait = block.getFieldValue("wait");
|
||||
return `await playSound(${name}, ${wait});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["stop_sound"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("stop sound")
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
function() {
|
||||
return getAvailableSounds();
|
||||
}
|
||||
), "SOUND_NAME");
|
||||
this.setColour("#ff66ba");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["stop_sound"] = function (block, generator) {
|
||||
var name = "'" + block.getFieldValue("SOUND_NAME") + "'";
|
||||
return `stopSound(${name});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["stop_all_sounds"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("stop")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["all", "false"],
|
||||
["my", "true"],
|
||||
]),
|
||||
"who"
|
||||
)
|
||||
.appendField("sounds");
|
||||
this.setColour("#ff66ba");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["stop_all_sounds"] = function (block) {
|
||||
var who = block.getFieldValue("who");
|
||||
var code = `stopAllSounds(${who});\n`;
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_sound_property"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("value")
|
||||
.setCheck("Number")
|
||||
.appendField("set")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["volume", "volume"],
|
||||
["speed", "speed"],
|
||||
]),
|
||||
"property"
|
||||
)
|
||||
.appendField("to");
|
||||
this.appendDummyInput().appendField("%");
|
||||
this.setColour("#ff66ba");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_sound_property"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
var value = generator.valueToCode(
|
||||
block,
|
||||
"value",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
);
|
||||
var property = block.getFieldValue("property");
|
||||
return `setSoundProperty("${property}", ${value});\n`;
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_sound_property"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["volume", "volume"],
|
||||
["speed", "speed"],
|
||||
]),
|
||||
"property"
|
||||
);
|
||||
this.setColour("#ff66ba");
|
||||
this.setOutput(true, "Number");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_sound_property"] = function (block) {
|
||||
var property = block.getFieldValue("property");
|
||||
return [`soundProperties["${property}"]`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
137
src/blocks/system.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
const normalKeys = [
|
||||
..."abcdefghijklmnopqrstuvwxyz",
|
||||
..."abcdefghijklmnopqrstuvwxyz0123456789".toUpperCase(),
|
||||
];
|
||||
|
||||
Blockly.Blocks["key_pressed"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("is")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["any", "any"],
|
||||
["space", " "],
|
||||
["enter", "Enter"],
|
||||
["escape", "Escape"],
|
||||
["up arrow", "ArrowUp"],
|
||||
["down arrow", "ArrowDown"],
|
||||
["left arrow", "ArrowLeft"],
|
||||
["right arrow", "ArrowRight"],
|
||||
...normalKeys.map((i) => [i, i]),
|
||||
]),
|
||||
"KEY"
|
||||
)
|
||||
.appendField("key down");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["get_mouse_position"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("mouse")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["x", "x"],
|
||||
["y", "y"],
|
||||
]),
|
||||
"MENU"
|
||||
);
|
||||
this.setOutput(true, "Number");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["mouse_button_pressed"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("is")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["left", "0"],
|
||||
["middle", "1"],
|
||||
["right", "2"],
|
||||
["back", "3"],
|
||||
["forward", "4"],
|
||||
["any", "any"],
|
||||
]),
|
||||
"BUTTON"
|
||||
)
|
||||
.appendField("mouse button down");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["all_keys_pressed"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("keys currently down");
|
||||
this.setOutput(true, "Array");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["mouse_over"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField("is cursor over me");
|
||||
this.setOutput(true, "Boolean");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["key_pressed"] = function (block, generator) {
|
||||
const key = block.getFieldValue("KEY");
|
||||
const safeKey = generator.quote_(key);
|
||||
return [`isKeyPressed(${safeKey})`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_mouse_position"] = function (block) {
|
||||
const menu = block.getFieldValue("MENU");
|
||||
return [`getMousePosition("${menu}")`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["mouse_button_pressed"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const button = block.getFieldValue("BUTTON");
|
||||
const safeButton = generator.quote_(button);
|
||||
return [`isMouseButtonPressed(${safeButton})`, BlocklyJS.Order.NONE];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["all_keys_pressed"] = () => [
|
||||
"Object.keys(keysPressed).filter(k => keysPressed[k])",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["mouse_over"] = () => [
|
||||
"isMouseTouchingSprite()",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
Blockly.Blocks["window_size"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField("window")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["width", "width"],
|
||||
["height", "height"],
|
||||
]),
|
||||
"MENU"
|
||||
);
|
||||
this.setOutput(true, "Number");
|
||||
this.setColour("#5CB1D6");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["window_size"] = function (block) {
|
||||
return [
|
||||
`window.inner${block.getFieldValue("MENU") === "width" ? "Width" : "Height"}`,
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
};
|
||||
291
src/blocks/tween.js
Normal file
@@ -0,0 +1,291 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
|
||||
const TweenEasing = {
|
||||
InLinear: (t) => t,
|
||||
OutLinear: (t) => t,
|
||||
InOutLinear: (t) => t,
|
||||
InSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
|
||||
OutSine: (t) => Math.sin((t * Math.PI) / 2),
|
||||
InOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
||||
InQuad: (t) => t * t,
|
||||
OutQuad: (t) => 1 - (1 - t) * (1 - t),
|
||||
InOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
|
||||
InCubic: (t) => t * t * t,
|
||||
OutCubic: (t) => 1 - Math.pow(1 - t, 3),
|
||||
InOutCubic: (t) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
||||
InQuart: (t) => t * t * t * t,
|
||||
OutQuart: (t) => 1 - Math.pow(1 - t, 4),
|
||||
InOutQuart: (t) =>
|
||||
t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2,
|
||||
InQuint: (t) => t * t * t * t * t,
|
||||
OutQuint: (t) => 1 - Math.pow(1 - t, 5),
|
||||
InOutQuint: (t) =>
|
||||
t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2,
|
||||
InExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
|
||||
OutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
|
||||
InOutExpo: (t) => {
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
return t < 0.5
|
||||
? Math.pow(2, 20 * t - 10) / 2
|
||||
: (2 - Math.pow(2, -20 * t + 10)) / 2;
|
||||
},
|
||||
InCirc: (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)),
|
||||
OutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
|
||||
InOutCirc: (t) =>
|
||||
t < 0.5
|
||||
? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
|
||||
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
|
||||
InBack: (t) => {
|
||||
const c1 = 1.70158,
|
||||
c3 = c1 + 1;
|
||||
return c3 * t * t * t - c1 * t * t;
|
||||
},
|
||||
OutBack: (t) => {
|
||||
const c1 = 1.70158,
|
||||
c3 = c1 + 1;
|
||||
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
|
||||
},
|
||||
InOutBack: (t) => {
|
||||
const c1 = 1.70158,
|
||||
c2 = c1 * 1.525;
|
||||
return t < 0.5
|
||||
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
|
||||
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (2 * t - 2) + c2) + 2) / 2;
|
||||
},
|
||||
InElastic: (t) => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
|
||||
},
|
||||
OutElastic: (t) => {
|
||||
const c4 = (2 * Math.PI) / 3;
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
|
||||
},
|
||||
InOutElastic: (t) => {
|
||||
const c5 = (2 * Math.PI) / 4.5;
|
||||
if (t === 0) return 0;
|
||||
if (t === 1) return 1;
|
||||
return t < 0.5
|
||||
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
|
||||
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
|
||||
},
|
||||
InBounce: (t) => 1 - TweenEasing.OutBounce(1 - t),
|
||||
OutBounce: (t) => {
|
||||
const n1 = 7.5625,
|
||||
d1 = 2.75;
|
||||
if (t < 1 / d1) {
|
||||
return n1 * t * t;
|
||||
} else if (t < 2 / d1) {
|
||||
return n1 * (t -= 1.5 / d1) * t + 0.75;
|
||||
} else if (t < 2.5 / d1) {
|
||||
return n1 * (t -= 2.25 / d1) * t + 0.9375;
|
||||
} else {
|
||||
return n1 * (t -= 2.625 / d1) * t + 0.984375;
|
||||
}
|
||||
},
|
||||
InOutBounce: (t) =>
|
||||
t < 0.5
|
||||
? (1 - TweenEasing.OutBounce(1 - 2 * t)) / 2
|
||||
: (1 + TweenEasing.OutBounce(2 * t - 1)) / 2,
|
||||
};
|
||||
Object.defineProperty(window, "TweenEasing", {
|
||||
value: Object.freeze(TweenEasing),
|
||||
configurable: false,
|
||||
writable: false,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Blockly.Blocks["tween_block"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("FROM").setCheck("Number").appendField("tween from");
|
||||
this.appendValueInput("TO").setCheck("Number").appendField("to");
|
||||
this.appendDummyInput().appendField("in");
|
||||
this.appendValueInput("DURATION").setCheck("Number");
|
||||
this.appendDummyInput().appendField("seconds using");
|
||||
this.appendDummyInput()
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["linear", "Linear"],
|
||||
["sine", "Sine"],
|
||||
["quadratic", "Quad"],
|
||||
["cubic", "Cubic"],
|
||||
["quartic", "Quart"],
|
||||
["quintic", "Quint"],
|
||||
["expo", "Expo"],
|
||||
["circ", "Circ"],
|
||||
["back", "Back"],
|
||||
["elastic", "Elastic"],
|
||||
["bounce", "Bounce"],
|
||||
]),
|
||||
"EASING_TYPE"
|
||||
)
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["in", "In"],
|
||||
["out", "Out"],
|
||||
["in-out", "InOut"],
|
||||
]),
|
||||
"EASING_MODE"
|
||||
);
|
||||
|
||||
this.appendStatementInput("DO").setCheck("default");
|
||||
this.appendDummyInput()
|
||||
.setAlign(1)
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["wait", "WAIT"],
|
||||
["don't wait", "DONT_WAIT"],
|
||||
]),
|
||||
"WAIT_MODE"
|
||||
)
|
||||
.appendField("until finished");
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#32a2c0");
|
||||
this.setTooltip(
|
||||
"Tween a value from one number to another over time using easing"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["tween_block"] = function (block, generator) {
|
||||
const easingType = block.getFieldValue("EASING_TYPE");
|
||||
const easingMode = block.getFieldValue("EASING_MODE");
|
||||
const from =
|
||||
generator.valueToCode(block, "FROM", BlocklyJS.Order.ATOMIC) ||
|
||||
"0";
|
||||
const to =
|
||||
generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0";
|
||||
const duration =
|
||||
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) ||
|
||||
"1";
|
||||
const waitMode = block.getFieldValue("WAIT_MODE");
|
||||
|
||||
let branch = BlocklyJS.javascriptGenerator.statementToCode(block, "DO");
|
||||
branch = BlocklyJS.javascriptGenerator.addLoopTrap(branch, block);
|
||||
|
||||
const code = `await startTween({
|
||||
from: ${from},
|
||||
to: ${to},
|
||||
duration: ${duration},
|
||||
easing: "${easingMode + easingType}",
|
||||
wait: ${waitMode === "WAIT"},
|
||||
onUpdate: async (tweenValue) => {
|
||||
${branch} }
|
||||
});\n`;
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
Blockly.Blocks["tween_block_value"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput("name").appendField("current tween value");
|
||||
this.setInputsInline(true);
|
||||
this.setColour("#32a2c0");
|
||||
this.setOutput(true, "Number");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["tween_block_value"] = () => [
|
||||
"tweenValue",
|
||||
BlocklyJS.Order.NONE,
|
||||
];
|
||||
|
||||
Blockly.Blocks["tween_sprite_property"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("TO")
|
||||
.setCheck("Number")
|
||||
.appendField("tween")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["x position", "x"],
|
||||
["y position", "y"],
|
||||
["angle", "angle"],
|
||||
["size", "size"],
|
||||
]),
|
||||
"PROPERTY"
|
||||
)
|
||||
.appendField("to");
|
||||
this.appendValueInput("DURATION").setCheck("Number").appendField("in");
|
||||
this.appendDummyInput()
|
||||
.appendField("seconds using")
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["linear", "Linear"],
|
||||
["sine", "Sine"],
|
||||
["quadratic", "Quad"],
|
||||
["cubic", "Cubic"],
|
||||
["quartic", "Quart"],
|
||||
["quintic", "Quint"],
|
||||
["expo", "Expo"],
|
||||
["circ", "Circ"],
|
||||
["back", "Back"],
|
||||
["elastic", "Elastic"],
|
||||
["bounce", "Bounce"],
|
||||
]),
|
||||
"EASING_TYPE"
|
||||
)
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
["in", "In"],
|
||||
["out", "Out"],
|
||||
["in-out", "InOut"],
|
||||
]),
|
||||
"EASING_MODE"
|
||||
);
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setColour("#32a2c0");
|
||||
this.setTooltip(
|
||||
"Tween a sprite property to a target value over time using easing"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["tween_sprite_property"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const prop = block.getFieldValue("PROPERTY");
|
||||
const to =
|
||||
generator.valueToCode(block, "TO", BlocklyJS.Order.ATOMIC) || "0";
|
||||
const duration =
|
||||
generator.valueToCode(block, "DURATION", BlocklyJS.Order.ATOMIC) ||
|
||||
"1";
|
||||
const easingType = block.getFieldValue("EASING_TYPE");
|
||||
const easingMode = block.getFieldValue("EASING_MODE");
|
||||
|
||||
let fromGetter, setter;
|
||||
if (prop === "size") {
|
||||
fromGetter = "getSpriteScale()";
|
||||
setter = `setSize(tweenValue, false)`;
|
||||
} else if (prop === "angle") {
|
||||
fromGetter = `sprite.angle`;
|
||||
setter = `setAngle(tweenValue, false)`;
|
||||
} else {
|
||||
fromGetter = `sprite["${prop}"]`;
|
||||
setter = `sprite["${prop}"] = tweenValue`;
|
||||
}
|
||||
|
||||
setter = generator.addLoopTrap(setter, block);
|
||||
|
||||
const code = `await startTween({
|
||||
from: ${fromGetter},
|
||||
to: ${to},
|
||||
duration: ${duration},
|
||||
easing: "${easingMode + easingType}",
|
||||
onUpdate: async (tweenValue) => {
|
||||
${setter};
|
||||
}
|
||||
});\n`;
|
||||
|
||||
return code;
|
||||
};
|
||||
85
src/blocks/variable.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
import { projectVariables } from "../scripts/editor";
|
||||
|
||||
function getVariables() {
|
||||
if (Object.keys(projectVariables).length === 0)
|
||||
return [["unknown", "unknown"]];
|
||||
else return Object.keys(projectVariables).map((name) => [name, name]);
|
||||
}
|
||||
|
||||
Blockly.Blocks["get_global_var"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField(
|
||||
new Blockly.FieldDropdown(() => getVariables()),
|
||||
"VAR"
|
||||
);
|
||||
this.setOutput(true);
|
||||
this.setTooltip("Get a global variable");
|
||||
this.setStyle("variable_blocks");
|
||||
this.customContextMenu = function (options) {
|
||||
const varName = this.getFieldValue("VAR");
|
||||
options.push({
|
||||
text: `Delete "${varName}" variable`,
|
||||
enabled: true,
|
||||
callback: () => {
|
||||
delete projectVariables[varName];
|
||||
this.workspace.refreshToolboxSelection();
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["set_global_var"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck(null)
|
||||
.appendField("set")
|
||||
.appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR")
|
||||
.appendField("to");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("variable_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks["change_global_var"] = {
|
||||
init: function () {
|
||||
this.appendValueInput("VALUE")
|
||||
.setCheck("Number")
|
||||
.appendField("change")
|
||||
.appendField(new Blockly.FieldDropdown(() => getVariables()), "VAR")
|
||||
.appendField("by");
|
||||
this.setPreviousStatement(true, "default");
|
||||
this.setNextStatement(true, "default");
|
||||
this.setStyle("variable_blocks");
|
||||
},
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["get_global_var"] = function (block) {
|
||||
const name = block.getFieldValue("VAR");
|
||||
return [`projectVariables["${name}"]`, BlocklyJS.Order.ATOMIC];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["set_global_var"] = function (block) {
|
||||
const name = block.getFieldValue("VAR");
|
||||
const value =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
BlocklyJS.Order.ASSIGNMENT
|
||||
) || "0";
|
||||
return `projectVariables["${name}"] = ${value};\n`;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["change_global_var"] = function (block) {
|
||||
const name = block.getFieldValue("VAR");
|
||||
const value =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
"VALUE",
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || "0";
|
||||
return `projectVariables["${name}"] += ${value};\n`;
|
||||
};
|
||||
3
src/cache.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const cache = {
|
||||
user: null,
|
||||
};
|
||||
5
src/config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const localhost = window.location.hostname === "localhost";
|
||||
|
||||
export default {
|
||||
apiUrl: localhost ? "http://localhost:3000" : "https://rarry-api-production.up.railway.app",
|
||||
};
|
||||
415
src/editor.css
Normal file
@@ -0,0 +1,415 @@
|
||||
#blocklyDiv {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html.stageLeft .main {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 66.66%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 2px solid var(--color3);
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--color3);
|
||||
}
|
||||
|
||||
button.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
background-color: var(--color2);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button.tab-button.inactive {
|
||||
background-color: var(--color1);
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-section {
|
||||
padding: 1rem;
|
||||
background-color: var(--color1);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sound-container,
|
||||
.costume-container {
|
||||
padding: 8px;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--color2);
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
#stage-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 56.25%;
|
||||
}
|
||||
|
||||
#stage-div.fullscreen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
z-index: 9999;
|
||||
background: var(--color1);
|
||||
}
|
||||
|
||||
#stage canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 33.33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#stage-controls {
|
||||
background-color: var(--color1);
|
||||
padding: 0.5rem;
|
||||
border-bottom: 2px solid var(--color3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#stage-controls button {
|
||||
padding: 0.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 25%;
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
#stage-controls button:hover {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
#stage-controls button.active {
|
||||
outline: 2px solid #2acc65a6;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
#stage-controls button img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.dark #fullscreen-button img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
#sprite-info {
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#sprite-info p {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
#sprite-info p:not(:last-child) {
|
||||
padding-right: 5px;
|
||||
border-right: 2px solid var(--color2);
|
||||
}
|
||||
|
||||
#sprites-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--color1);
|
||||
padding: 0.5rem;
|
||||
border-top: 2px solid var(--color3);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#sprites-section div#sprite-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#sprites-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
max-height: 9rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#sprites-list div {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
background-color: var(--color2);
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sprites-list div.active {
|
||||
background-color: var(--primary);
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
#sprites-section-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#costumes-list,
|
||||
#sounds-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#extensionButton {
|
||||
padding: 3px;
|
||||
margin: 0 5px;
|
||||
margin-top: auto;
|
||||
background: var(--primary);
|
||||
order: 9009;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.extensions-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
background-color: var(--color1);
|
||||
}
|
||||
|
||||
.extensions-popup .extensions-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.extensions-list div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color2);
|
||||
}
|
||||
|
||||
.extensions-list div img {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.extensions-list div h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.extensions-popup header {
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.blocklyMainBackground {
|
||||
stroke-width: 0;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.blocklyToolboxCategoryGroup {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html.dark .blocklyToolboxCategoryIcon {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.blocklyToolboxCategory {
|
||||
height: initial;
|
||||
padding: 3px 0;
|
||||
transition: background-color 0.2s ease;
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.blocklyToolboxCategory.blocklyToolboxSelected {
|
||||
background-color: rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
html.dark .blocklyToolboxCategory.blocklyToolboxSelected {
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.draggingBlocklyToolboxCategory .blocklyToolboxCategory {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.blocklyToolboxCategoryLabel {
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.blocklyTreeSeparator {
|
||||
border-bottom-color: var(--dark);
|
||||
}
|
||||
|
||||
.blocklyTreeRowContentContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.blocklyWidgetDiv .blocklyMenu {
|
||||
background-color: var(--color1);
|
||||
border-color: var(--color2);
|
||||
}
|
||||
|
||||
.blocklyMenuItem {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.blocklyMenuItemDisabled {
|
||||
color: var(--color4);
|
||||
}
|
||||
|
||||
.categoryBubble {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
#room-users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
#room-users div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
#room-users div img {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
.smallLabel {
|
||||
margin-left: auto;
|
||||
font-size: 0.8em;
|
||||
color: var(--dark-light);
|
||||
}
|
||||
#backdrops-section {
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#backdrops-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#backdrops-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
#backdrops-list > div {
|
||||
width: 60px;
|
||||
height: 45px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
#backdrops-list > div:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
#backdrops-list > div.active {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 1px var(--primary-color);
|
||||
}
|
||||
|
||||
#backdrops-list > div img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#backdrops-section-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#backdrops-section-buttons button {
|
||||
flex: 1;
|
||||
}
|
||||
248
src/functions/extensionManager.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
import { activeExtensions } from "../scripts/editor";
|
||||
import { Thread } from "./threads";
|
||||
|
||||
Thread.resetAll();
|
||||
|
||||
function textToBlock(block, text, fields) {
|
||||
const regex = /\[([^\]]+)\]/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text))) {
|
||||
const before = text.slice(lastIndex, match.index);
|
||||
if (before) {
|
||||
block.appendDummyInput().appendField(before.trim());
|
||||
}
|
||||
|
||||
const inputName = match[1].trim();
|
||||
const spec = fields?.[inputName];
|
||||
|
||||
if (spec?.kind === "statement") {
|
||||
block
|
||||
.appendStatementInput(inputName)
|
||||
.setCheck(spec?.accepts || "default");
|
||||
} else if (spec?.kind === "value") {
|
||||
block.appendValueInput(inputName).setCheck(spec?.type);
|
||||
} else if (spec?.kind === "menu") {
|
||||
const menuItems = spec.items.map((item) =>
|
||||
typeof item === "string" ? [item, item] : [item.text, item.value]
|
||||
);
|
||||
const field = new Blockly.FieldDropdown(menuItems);
|
||||
block.appendDummyInput().appendField(field, inputName);
|
||||
} else {
|
||||
block.appendDummyInput().appendField("[" + inputName + "]");
|
||||
}
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
const after = text.slice(lastIndex);
|
||||
if (after) {
|
||||
block.appendDummyInput().appendField(after.trim());
|
||||
}
|
||||
}
|
||||
|
||||
export function setupExtensions() {
|
||||
if (!window.extensions) {
|
||||
const backing = {};
|
||||
|
||||
const proxy = new Proxy(backing, {
|
||||
defineProperty(target, prop, descriptor) {
|
||||
if (prop in target)
|
||||
throw new Error(`Extension "${prop}" already defined`);
|
||||
return Reflect.defineProperty(target, prop, {
|
||||
...descriptor,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
},
|
||||
set(target, prop, value) {
|
||||
if (prop in target)
|
||||
throw new Error(`Extension "${prop}" is already defined and locked`);
|
||||
return Reflect.defineProperty(target, prop, {
|
||||
value,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
});
|
||||
},
|
||||
deleteProperty() {
|
||||
throw new Error("Extensions cannot be removed");
|
||||
},
|
||||
get(target, prop, receiver) {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
ownKeys(target) {
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "extensions", {
|
||||
value: proxy,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerExtension(extClass) {
|
||||
const ext = new extClass();
|
||||
const id = ext.id || ext.constructor.name;
|
||||
|
||||
if (activeExtensions.some((i) => (i?.id || i) === id)) {
|
||||
console.warn(`Extension ${id} already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
const coreDom = document.getElementById("toolbox");
|
||||
|
||||
const category = ext.registerCategory?.();
|
||||
let categoryEl = null;
|
||||
if (category) {
|
||||
categoryEl = document.createElement("category");
|
||||
categoryEl.setAttribute("name", category.name || "Extension");
|
||||
if (!category.color) category.color = "#888";
|
||||
categoryEl.setAttribute("colour", category.color);
|
||||
if (category.iconURI) categoryEl.setAttribute("iconURI", category.iconURI);
|
||||
}
|
||||
|
||||
const blocks = ext.registerBlocks?.() || [];
|
||||
const blockDefs = {};
|
||||
blocks.forEach((blockDef) => {
|
||||
if (!blockDef.id) {
|
||||
console.warn("Skipped registration of block with no ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const blockType = `${id}_${blockDef.id}`;
|
||||
blockDefs[blockType] = blockDef;
|
||||
Blockly.Blocks[blockType] = {
|
||||
init: function () {
|
||||
textToBlock(this, blockDef.text, blockDef.fields);
|
||||
|
||||
if (blockDef.type === "statement") {
|
||||
this.setPreviousStatement(true, blockDef.statementType || "default");
|
||||
this.setNextStatement(true, blockDef.statementType || "default");
|
||||
} else if (blockDef.type === "cap") {
|
||||
this.setPreviousStatement(true, blockDef.statementType || "default");
|
||||
} else if (blockDef.type === "output") {
|
||||
this.setOutput(true, blockDef.outputType);
|
||||
if (blockDef.outputShape) this.setOutputShape(blockDef.outputShape);
|
||||
}
|
||||
if (blockDef.tooltip) this.setTooltip(blockDef.tooltip);
|
||||
this.setInputsInline(true);
|
||||
this.setColour(blockDef?.color || category.color);
|
||||
},
|
||||
};
|
||||
|
||||
if (categoryEl) {
|
||||
const blockEl = document.createElement("block");
|
||||
blockEl.setAttribute("type", blockType);
|
||||
|
||||
for (const [name, spec] of Object.entries(blockDef.fields || {})) {
|
||||
if (spec?.kind === "menu") continue;
|
||||
|
||||
if (spec.default !== undefined && spec?.kind !== "statement") {
|
||||
const valueEl = document.createElement("value");
|
||||
valueEl.setAttribute("name", name.trim());
|
||||
|
||||
let shadowEl = null;
|
||||
|
||||
if (spec.type === "Number") {
|
||||
shadowEl = document.createElement("shadow");
|
||||
shadowEl.setAttribute("type", "math_number");
|
||||
|
||||
const fieldEl = document.createElement("field");
|
||||
fieldEl.setAttribute("name", "NUM");
|
||||
fieldEl.textContent = spec.default;
|
||||
shadowEl.appendChild(fieldEl);
|
||||
} else if (spec.type === "String") {
|
||||
shadowEl = document.createElement("shadow");
|
||||
shadowEl.setAttribute("type", "text");
|
||||
|
||||
const fieldEl = document.createElement("field");
|
||||
fieldEl.setAttribute("name", "TEXT");
|
||||
fieldEl.textContent = spec.default;
|
||||
shadowEl.appendChild(fieldEl);
|
||||
} else if (spec.type === "Boolean") {
|
||||
shadowEl = document.createElement("shadow");
|
||||
shadowEl.setAttribute("type", "logic_boolean");
|
||||
|
||||
const fieldEl = document.createElement("field");
|
||||
fieldEl.setAttribute("name", "BOOL");
|
||||
fieldEl.textContent = spec.default ? "TRUE" : "FALSE";
|
||||
shadowEl.appendChild(fieldEl);
|
||||
}
|
||||
|
||||
if (shadowEl) {
|
||||
valueEl.appendChild(shadowEl);
|
||||
}
|
||||
|
||||
blockEl.appendChild(valueEl);
|
||||
}
|
||||
}
|
||||
|
||||
categoryEl.appendChild(blockEl);
|
||||
}
|
||||
});
|
||||
|
||||
if (categoryEl) {
|
||||
coreDom.appendChild(categoryEl);
|
||||
Blockly.getMainWorkspace().updateToolbox(coreDom);
|
||||
}
|
||||
|
||||
const codeGen = ext.registerCode?.() || {};
|
||||
Object.entries(codeGen).forEach(([blockType, fn]) => {
|
||||
const fullType = `${id}_${blockType}`;
|
||||
|
||||
window.extensions[fullType] = fn;
|
||||
const def = blockDefs[fullType] || {};
|
||||
BlocklyJS.javascriptGenerator.forBlock[fullType] = function (block) {
|
||||
const inputs = {};
|
||||
|
||||
for (const input of block.inputList) {
|
||||
const name = input.name;
|
||||
let codeExpr;
|
||||
|
||||
if (input.type === 1 || input.type === 2) {
|
||||
codeExpr =
|
||||
BlocklyJS.javascriptGenerator.valueToCode(
|
||||
block,
|
||||
name,
|
||||
BlocklyJS.Order.ATOMIC
|
||||
) || undefined;
|
||||
if (codeExpr !== undefined) inputs[name] = codeExpr;
|
||||
} else if (input.type === 3) {
|
||||
codeExpr =
|
||||
BlocklyJS.javascriptGenerator.statementToCode(block, name) ||
|
||||
undefined;
|
||||
if (codeExpr !== undefined)
|
||||
inputs[name] = `async () => { ${codeExpr} }`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, spec] of Object.entries(def.fields || {})) {
|
||||
if (spec.kind === "menu") {
|
||||
const fieldVal = block.getFieldValue(name);
|
||||
if (fieldVal !== undefined) inputs[name] = JSON.stringify(fieldVal);
|
||||
}
|
||||
}
|
||||
|
||||
const argsParts = Object.entries(inputs).map(
|
||||
([k, v]) => `${JSON.stringify(k)}:${v}`
|
||||
);
|
||||
const args = `{${argsParts.join(",")}}`;
|
||||
const callCode = `extensions["${fullType}"](${args},Thread)`;
|
||||
|
||||
const finalCode = def.promise ? `await ${callCode}` : callCode;
|
||||
|
||||
if (block.outputConnection) return [finalCode, BlocklyJS.Order.NONE];
|
||||
else return finalCode + ";\n";
|
||||
};
|
||||
});
|
||||
|
||||
activeExtensions.push({ id, code: extClass.toString() });
|
||||
}
|
||||
334
src/functions/patches.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import * as Blockly from "blockly";
|
||||
import * as BlocklyJS from "blockly/javascript";
|
||||
import * as PIXI from "pixi.js-legacy";
|
||||
|
||||
BlocklyJS.javascriptGenerator.INFINITE_LOOP_TRAP =
|
||||
'if (stopped()) throw new Error("shouldStop");\nif (!fastExecution) await new Promise(r => setTimeout(r, 16));\n';
|
||||
|
||||
Blockly.VerticalFlyout.prototype.getFlyoutScale = () => 0.8;
|
||||
|
||||
[
|
||||
"controls_if",
|
||||
"controls_if_if",
|
||||
"controls_if_elseif",
|
||||
"controls_if_else",
|
||||
].forEach((type) => {
|
||||
Blockly.Blocks[type].init = (function (original) {
|
||||
return function () {
|
||||
original.call(this);
|
||||
this.setColour("#FFAB19");
|
||||
};
|
||||
})(Blockly.Blocks[type].init);
|
||||
});
|
||||
|
||||
Blockly.Blocks["controls_forEach"].init = (function (original) {
|
||||
return function () {
|
||||
original.call(this);
|
||||
this.setColour("#e35340");
|
||||
};
|
||||
})(Blockly.Blocks["controls_forEach"].init);
|
||||
|
||||
Blockly.Blocks["text"] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField(new Blockly.FieldTextInput(""), "TEXT");
|
||||
this.setOutput(true, "String");
|
||||
this.setStyle("text_blocks");
|
||||
this.setTooltip(Blockly.Msg["TEXT_TEXT_TOOLTIP"]);
|
||||
this.setHelpUrl(Blockly.Msg["TEXT_TEXT_HELPURL"]);
|
||||
|
||||
Blockly.Extensions.apply("parent_tooltip_when_inline", this, false);
|
||||
setTimeout(() => {
|
||||
if (!this.isShadow()) {
|
||||
Blockly.Extensions.apply("text_quotes", this, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Object.keys(Blockly.Blocks).forEach((type) => {
|
||||
const block = Blockly.Blocks[type];
|
||||
if (!block || typeof block.init !== "function") return;
|
||||
|
||||
const originalInit = block.init;
|
||||
block.init = function () {
|
||||
originalInit.call(this);
|
||||
if (this.previousConnection && this.previousConnection.check_ === null)
|
||||
this.setPreviousStatement(true, "default");
|
||||
if (this.nextConnection && this.nextConnection.check_ === null)
|
||||
this.setNextStatement(true, "default");
|
||||
};
|
||||
});
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["procedures_defnoreturn"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const procedureName = generator.getProcedureName(block.getFieldValue("NAME"));
|
||||
|
||||
let injectedCode = "";
|
||||
if (generator.STATEMENT_PREFIX) {
|
||||
injectedCode += generator.injectId(generator.STATEMENT_PREFIX, block);
|
||||
}
|
||||
if (generator.STATEMENT_SUFFIX) {
|
||||
injectedCode += generator.injectId(generator.STATEMENT_SUFFIX, block);
|
||||
}
|
||||
if (injectedCode) {
|
||||
injectedCode = generator.prefixLines(injectedCode, generator.INDENT);
|
||||
}
|
||||
|
||||
let loopTrap = "";
|
||||
if (generator.INFINITE_LOOP_TRAP) {
|
||||
loopTrap = generator.prefixLines(
|
||||
generator.injectId(generator.INFINITE_LOOP_TRAP, block),
|
||||
generator.INDENT
|
||||
);
|
||||
}
|
||||
|
||||
let bodyCode = "";
|
||||
if (block.getInput("STACK")) {
|
||||
bodyCode = generator.statementToCode(block, "STACK");
|
||||
}
|
||||
|
||||
let returnCode = "";
|
||||
if (block.getInput("RETURN")) {
|
||||
returnCode =
|
||||
generator.valueToCode(block, "RETURN", BlocklyJS.Order.NONE) || "";
|
||||
}
|
||||
|
||||
let returnWrapper = "";
|
||||
if (bodyCode && returnCode) {
|
||||
returnWrapper = injectedCode;
|
||||
}
|
||||
|
||||
if (returnCode) {
|
||||
returnCode = generator.INDENT + "return " + returnCode + ";\n";
|
||||
}
|
||||
|
||||
const args = [];
|
||||
const vars = block.getVars();
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
args[i] = generator.getVariableName(vars[i]);
|
||||
}
|
||||
|
||||
let code =
|
||||
"async function " +
|
||||
procedureName +
|
||||
"(" +
|
||||
args.join(", ") +
|
||||
") {\n" +
|
||||
injectedCode +
|
||||
loopTrap +
|
||||
bodyCode +
|
||||
returnWrapper +
|
||||
returnCode +
|
||||
"}";
|
||||
|
||||
code = generator.scrub_(block, code);
|
||||
generator.definitions_["%" + procedureName] = code;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["procedures_defreturn"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const procedureName = generator.getProcedureName(block.getFieldValue("NAME"));
|
||||
|
||||
let statementWrapper = "";
|
||||
if (generator.STATEMENT_PREFIX) {
|
||||
statementWrapper += generator.injectId(generator.STATEMENT_PREFIX, block);
|
||||
}
|
||||
if (generator.STATEMENT_SUFFIX) {
|
||||
statementWrapper += generator.injectId(generator.STATEMENT_SUFFIX, block);
|
||||
}
|
||||
if (statementWrapper) {
|
||||
statementWrapper = generator.prefixLines(
|
||||
statementWrapper,
|
||||
generator.INDENT
|
||||
);
|
||||
}
|
||||
|
||||
let loopTrapCode = "";
|
||||
if (generator.INFINITE_LOOP_TRAP) {
|
||||
loopTrapCode = generator.prefixLines(
|
||||
generator.injectId(generator.INFINITE_LOOP_TRAP, block),
|
||||
generator.INDENT
|
||||
);
|
||||
}
|
||||
|
||||
let bodyCode = "";
|
||||
if (block.getInput("STACK")) {
|
||||
bodyCode = generator.statementToCode(block, "STACK");
|
||||
}
|
||||
|
||||
let returnCode = "";
|
||||
if (block.getInput("RETURN")) {
|
||||
returnCode =
|
||||
generator.valueToCode(block, "RETURN", BlocklyJS.Order.NONE) || "";
|
||||
}
|
||||
|
||||
let returnWrapper = "";
|
||||
if (bodyCode && returnCode) {
|
||||
returnWrapper = statementWrapper;
|
||||
}
|
||||
|
||||
if (returnCode) {
|
||||
returnCode = generator.INDENT + "return " + returnCode + ";\n";
|
||||
}
|
||||
|
||||
const args = [];
|
||||
const vars = block.getVars();
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
args[i] = generator.getVariableName(vars[i]);
|
||||
}
|
||||
|
||||
let code =
|
||||
"async function " +
|
||||
procedureName +
|
||||
"(" +
|
||||
args.join(", ") +
|
||||
") {\n" +
|
||||
statementWrapper +
|
||||
loopTrapCode +
|
||||
bodyCode +
|
||||
returnWrapper +
|
||||
returnCode +
|
||||
"}";
|
||||
|
||||
code = generator.scrub_(block, code);
|
||||
generator.definitions_["%" + procedureName] = code;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["procedures_callreturn"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const procedureName = generator.getProcedureName(block.getFieldValue("NAME"));
|
||||
|
||||
const args = [];
|
||||
const vars = block.getVars();
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
args[i] =
|
||||
generator.valueToCode(block, "ARG" + i, BlocklyJS.Order.NONE) || "null";
|
||||
}
|
||||
|
||||
return [
|
||||
"await " + procedureName + "(" + args.join(", ") + ")",
|
||||
BlocklyJS.Order.FUNCTION_CALL,
|
||||
];
|
||||
};
|
||||
|
||||
BlocklyJS.javascriptGenerator.forBlock["procedures_callnoreturn"] = function (
|
||||
block,
|
||||
generator
|
||||
) {
|
||||
const code = generator.forBlock.procedures_callreturn(block, generator)[0];
|
||||
return code + ";\n";
|
||||
};
|
||||
|
||||
export const SpriteChangeEvents = new PIXI.utils.EventEmitter();
|
||||
|
||||
const originalX = Object.getOwnPropertyDescriptor(
|
||||
PIXI.DisplayObject.prototype,
|
||||
"x"
|
||||
);
|
||||
const originalY = Object.getOwnPropertyDescriptor(
|
||||
PIXI.DisplayObject.prototype,
|
||||
"y"
|
||||
);
|
||||
const originalAngle = Object.getOwnPropertyDescriptor(
|
||||
PIXI.DisplayObject.prototype,
|
||||
"angle"
|
||||
);
|
||||
const originalTexture = Object.getOwnPropertyDescriptor(
|
||||
PIXI.Sprite.prototype,
|
||||
"texture"
|
||||
);
|
||||
|
||||
Object.defineProperty(PIXI.Sprite.prototype, "x", {
|
||||
get() {
|
||||
return originalX.get.call(this);
|
||||
},
|
||||
set(value) {
|
||||
if (this.x !== value) {
|
||||
originalX.set.call(this, value);
|
||||
SpriteChangeEvents.emit("positionChanged", this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(PIXI.Sprite.prototype, "y", {
|
||||
get() {
|
||||
return originalY.get.call(this);
|
||||
},
|
||||
set(value) {
|
||||
if (this.y !== value) {
|
||||
originalY.set.call(this, value);
|
||||
SpriteChangeEvents.emit("positionChanged", this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
PIXI.Sprite.prototype.setPosition = function (x = null, y = null, add = false) {
|
||||
const newX = x !== null ? (add ? this.x + x : x) : this.x;
|
||||
const newY = y !== null ? (add ? this.y + y : y) : this.y;
|
||||
if (this.x === newX && this.y === newY) return;
|
||||
originalX.set.call(this, newX);
|
||||
originalY.set.call(this, newY);
|
||||
SpriteChangeEvents.emit("positionChanged", this);
|
||||
};
|
||||
|
||||
Object.defineProperty(PIXI.Sprite.prototype, "angle", {
|
||||
get() {
|
||||
return originalAngle.get.call(this);
|
||||
},
|
||||
set(value) {
|
||||
if (this.angle !== value) {
|
||||
originalAngle.set.call(this, value);
|
||||
SpriteChangeEvents.emit("positionChanged", this);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(PIXI.Sprite.prototype, "texture", {
|
||||
get() {
|
||||
return originalTexture.get.call(this);
|
||||
},
|
||||
set(value) {
|
||||
if (this.constructor === PIXI.Sprite && this.texture !== value) {
|
||||
originalTexture.set.call(this, value);
|
||||
SpriteChangeEvents.emit("textureChanged", this);
|
||||
} else {
|
||||
originalTexture.set.call(this, value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const originalObsPointSet = PIXI.ObservablePoint.prototype.set;
|
||||
|
||||
PIXI.ObservablePoint.prototype.set = function (x, y) {
|
||||
const result = originalObsPointSet.call(this, x, y);
|
||||
if (this._parentScaleEvent) {
|
||||
SpriteChangeEvents.emit("scaleChanged", this._parentScaleEvent);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
class ToolboxBubbleCategory extends Blockly.ToolboxCategory {
|
||||
createIconDom_() {
|
||||
const element = document.createElement("div");
|
||||
element.classList.add("categoryBubble");
|
||||
element.style.backgroundColor = this.colour_;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
Blockly.registry.register(
|
||||
Blockly.registry.Type.TOOLBOX_ITEM,
|
||||
Blockly.ToolboxCategory.registrationName,
|
||||
ToolboxBubbleCategory,
|
||||
true
|
||||
);
|
||||
217
src/functions/render.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as Blockly from "blockly";
|
||||
|
||||
class CustomConstantProvider extends Blockly.zelos.ConstantProvider {
|
||||
init() {
|
||||
super.init();
|
||||
this.BOWL = this.makeBowl();
|
||||
this.PILLOW = this.makePillow();
|
||||
this.SPIKEY = this.makeSpikey();
|
||||
}
|
||||
|
||||
makeBowl() {
|
||||
const maxW = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH;
|
||||
const maxH = maxW * 2;
|
||||
const roundedCopy = this.ROUNDED;
|
||||
|
||||
function makeMainPath(blockHeight, up, right) {
|
||||
const extra = blockHeight > maxH ? blockHeight - maxH : 0;
|
||||
const h_ = Math.min(blockHeight, maxH);
|
||||
const h = h_ + extra;
|
||||
const radius = h / 2;
|
||||
const radiusH = Math.min(h_ / 2, maxH);
|
||||
const dirR = right ? 1 : -1;
|
||||
const dirU = up ? -1 : 1;
|
||||
|
||||
return `
|
||||
h ${radiusH * dirR}
|
||||
q ${(h_ / 4) * -dirR} ${radius * dirU} 0 ${h * dirU}
|
||||
h ${radiusH * -dirR}
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.SHAPES.ROUND,
|
||||
isDynamic: true,
|
||||
width(h) {
|
||||
const half = h / 2;
|
||||
return half > maxW ? maxW : half;
|
||||
},
|
||||
height(h) {
|
||||
return h;
|
||||
},
|
||||
connectionOffsetY(h) {
|
||||
return h / 2;
|
||||
},
|
||||
connectionOffsetX(w) {
|
||||
return -w;
|
||||
},
|
||||
pathDown(h) {
|
||||
return makeMainPath(h, false, false);
|
||||
},
|
||||
pathUp(h) {
|
||||
return makeMainPath(h, true, false);
|
||||
},
|
||||
pathRightDown(h) {
|
||||
return roundedCopy.pathRightDown(h);
|
||||
},
|
||||
pathRightUp(h) {
|
||||
return roundedCopy.pathRightUp(h);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
makePillow() {
|
||||
const maxWidth = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH;
|
||||
const maxHeight = maxWidth * 2;
|
||||
|
||||
function makeMainPath(blockHeight, up, right) {
|
||||
const remainingHeight =
|
||||
blockHeight > maxHeight ? blockHeight - maxHeight : 0;
|
||||
const height = blockHeight > maxHeight ? maxHeight : blockHeight;
|
||||
const radius = height / 8;
|
||||
|
||||
const dirRight = right ? 1 : -1;
|
||||
const dirUp = up ? -1 : 1;
|
||||
|
||||
const radiusW = radius * dirRight;
|
||||
const radiusH = radius * dirUp;
|
||||
|
||||
return `
|
||||
h ${radiusW}
|
||||
q ${radiusW} 0 ${radiusW} ${radiusH}
|
||||
q 0 ${radiusH} ${radiusW} ${radiusH}
|
||||
q ${radiusW} 0 ${radiusW} ${radiusH}
|
||||
v ${(remainingHeight + height - radius * 6) * dirUp}
|
||||
q 0 ${radiusH} ${-radiusW} ${radiusH}
|
||||
q ${-radiusW} 0 ${-radiusW} ${radiusH}
|
||||
q 0 ${radiusH} ${-radiusW} ${radiusH}
|
||||
h ${-radiusW}
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.SHAPES.HEXAGONAL,
|
||||
isDynamic: true,
|
||||
width(height) {
|
||||
const halfHeight = height / 2;
|
||||
return halfHeight > maxWidth ? maxWidth : halfHeight;
|
||||
},
|
||||
height(height) {
|
||||
return height;
|
||||
},
|
||||
connectionOffsetY(connectionHeight) {
|
||||
return connectionHeight / 2;
|
||||
},
|
||||
connectionOffsetX(connectionWidth) {
|
||||
return -connectionWidth;
|
||||
},
|
||||
pathDown(height) {
|
||||
return makeMainPath(height, false, false);
|
||||
},
|
||||
pathUp(height) {
|
||||
return makeMainPath(height, true, false);
|
||||
},
|
||||
pathRightDown(height) {
|
||||
return makeMainPath(height, false, true);
|
||||
},
|
||||
pathRightUp(height) {
|
||||
return makeMainPath(height, false, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
makeSpikey() {
|
||||
const maxW = this.MAX_DYNAMIC_CONNECTION_SHAPE_WIDTH;
|
||||
const maxH = maxW * 2;
|
||||
|
||||
function makeMainPath(blockHeight, up, right) {
|
||||
const extra = blockHeight > maxH ? blockHeight - maxH : 0;
|
||||
const h_ = Math.min(blockHeight, maxH);
|
||||
const h = h_ + extra;
|
||||
const radius = h / 4;
|
||||
const radiusH = Math.min(h_ / 4, maxH);
|
||||
const dirR = right ? 1 : -1;
|
||||
const dirU = up ? -1 : 1;
|
||||
|
||||
return `
|
||||
h ${2 * radiusH * dirR}
|
||||
l ${radiusH * -dirR} ${radius * dirU}
|
||||
l ${radiusH * dirR} ${radius * dirU}
|
||||
l ${radiusH * -dirR} ${radius * dirU}
|
||||
l ${radiusH * dirR} ${radius * dirU}
|
||||
h ${2 * radiusH * -dirR}
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: this.SHAPES.HEXAGONAL,
|
||||
isDynamic: true,
|
||||
width(h) {
|
||||
const half = h / 2;
|
||||
return half > maxW ? maxW : half;
|
||||
},
|
||||
height(h) {
|
||||
return h;
|
||||
},
|
||||
connectionOffsetY(h) {
|
||||
return h / 2;
|
||||
},
|
||||
connectionOffsetX(w) {
|
||||
return -w;
|
||||
},
|
||||
pathDown(h) {
|
||||
return makeMainPath(h, false, false);
|
||||
},
|
||||
pathUp(h) {
|
||||
return makeMainPath(h, true, false);
|
||||
},
|
||||
pathRightDown(h) {
|
||||
return makeMainPath(h, false, true);
|
||||
},
|
||||
pathRightUp(h) {
|
||||
return makeMainPath(h, true, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Blockly.RenderedConnection} connection
|
||||
*/
|
||||
shapeFor(connection) {
|
||||
let checks = connection.getCheck() ?? [];
|
||||
if (!checks && connection.targetConnection)
|
||||
checks = connection.targetConnection.getCheck() ?? [];
|
||||
let outputShape = connection.sourceBlock_.getOutputShape();
|
||||
|
||||
if (connection.type === 1 || connection.type === 2) {
|
||||
if (
|
||||
(checks.includes("Array") || outputShape === 4) &&
|
||||
!["text_length", "text_isEmpty"].includes(connection.sourceBlock_.type)
|
||||
) {
|
||||
return this.BOWL;
|
||||
} else if (checks.includes("Object") || outputShape === 5) {
|
||||
return this.PILLOW;
|
||||
} else if (checks.includes("Set") || outputShape === 6) {
|
||||
return this.SPIKEY;
|
||||
} /*else if (
|
||||
checks.includes("String") &&
|
||||
connection?.sourceBlock_?.isShadow() &&
|
||||
connection?.targetConnection?.shadowState?.type === "text"
|
||||
) {
|
||||
return this.SQUARED;
|
||||
}*/
|
||||
}
|
||||
|
||||
return super.shapeFor(connection);
|
||||
}
|
||||
}
|
||||
|
||||
export default class CustomRenderer extends Blockly.zelos.Renderer {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
makeConstants_() {
|
||||
return new CustomConstantProvider();
|
||||
}
|
||||
}
|
||||
441
src/functions/runCode.js
Normal file
@@ -0,0 +1,441 @@
|
||||
import * as PIXI from "pixi.js-legacy";
|
||||
import { calculateBubblePosition, projectVariables } from "../scripts/editor";
|
||||
import { Thread } from "./threads";
|
||||
import { promiseWithAbort } from "./utils";
|
||||
|
||||
const BUBBLE_PADDING = 10;
|
||||
const BUBBLE_TAIL_HEIGHT = 15;
|
||||
const BUBBLE_TAIL_WIDTH = 15;
|
||||
const BUBBLE_COLOR = 0xffffff;
|
||||
const BUBBLE_TEXTSTYLE = new PIXI.TextStyle({ fill: 0x000000, fontSize: 24 });
|
||||
const LINE_COLOR = 0xbdc1c7;
|
||||
|
||||
export function runCodeWithFunctions({
|
||||
code,
|
||||
projectStartedTime,
|
||||
spriteData,
|
||||
app,
|
||||
eventRegistry,
|
||||
mouseButtonsPressed,
|
||||
keysPressed,
|
||||
playingSounds,
|
||||
runningScripts,
|
||||
signal,
|
||||
penGraphics,
|
||||
activeEventThreads,
|
||||
}) {
|
||||
Thread.resetAll();
|
||||
let fastExecution = false;
|
||||
|
||||
const sprite = spriteData.pixiSprite;
|
||||
const renderer = app.renderer;
|
||||
const stage = app.stage;
|
||||
const costumeMap = new Map(
|
||||
(spriteData.costumes || []).map((c) => [c.name, c])
|
||||
);
|
||||
const soundMap = new Map((spriteData.sounds || []).map((s) => [s.name, s]));
|
||||
const extensions = window.extensions;
|
||||
const MyFunctions = {};
|
||||
|
||||
function stopped() {
|
||||
return signal.aborted === true;
|
||||
}
|
||||
|
||||
function registerEvent(type, key, callback) {
|
||||
if (stopped()) return;
|
||||
|
||||
const entry = {
|
||||
type,
|
||||
cb: async () => {
|
||||
if (stopped()) return;
|
||||
|
||||
const threadId = Thread.create();
|
||||
Thread.enter(threadId);
|
||||
activeEventThreads.count++;
|
||||
|
||||
try {
|
||||
const result = await promiseWithAbort(
|
||||
() => callback(Thread.getCurrentContext()),
|
||||
signal
|
||||
);
|
||||
if (result === "shouldStop" || stopped()) return;
|
||||
} catch (err) {
|
||||
if (err.message !== "shouldStop") console.error(err);
|
||||
} finally {
|
||||
Thread.exit();
|
||||
activeEventThreads.count--;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "flag":
|
||||
eventRegistry.flag.push(entry);
|
||||
break;
|
||||
case "key":
|
||||
if (!eventRegistry.key.has(key)) eventRegistry.key.set(key, []);
|
||||
eventRegistry.key.get(key).push(entry);
|
||||
break;
|
||||
case "stageClick":
|
||||
eventRegistry.stageClick.push(entry);
|
||||
break;
|
||||
case "timer":
|
||||
eventRegistry.timer.push({ ...entry, value: key });
|
||||
break;
|
||||
case "interval":
|
||||
eventRegistry.interval.push({ ...entry, seconds: key });
|
||||
break;
|
||||
case "custom":
|
||||
if (!eventRegistry.custom.has(key)) eventRegistry.custom.set(key, []);
|
||||
eventRegistry.custom.get(key).push(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCustomEvent(eventName) {
|
||||
const entries = eventRegistry.custom.get(eventName);
|
||||
if (!entries) return;
|
||||
for (const entry of entries) {
|
||||
entry.cb();
|
||||
}
|
||||
}
|
||||
|
||||
function moveSteps(steps = 0) {
|
||||
const { rotation: a } = sprite;
|
||||
sprite.x += Math.cos(a) * steps;
|
||||
sprite.y += Math.sin(a) * steps;
|
||||
}
|
||||
|
||||
function getMousePosition(menu) {
|
||||
const mouse = renderer.events.pointer.global;
|
||||
if (menu === "x")
|
||||
return Math.round((mouse.x - renderer.width / 2) / stage.scale.x);
|
||||
else if (menu === "y")
|
||||
return -Math.round((mouse.y - renderer.height / 2) / stage.scale.y);
|
||||
}
|
||||
|
||||
function sayMessage(message, seconds) {
|
||||
if (stopped()) return;
|
||||
|
||||
message = String(message ?? "");
|
||||
if (!message) return;
|
||||
|
||||
if (!spriteData.currentBubble) {
|
||||
const bubble = new PIXI.Graphics();
|
||||
const text = new PIXI.Text("", BUBBLE_TEXTSTYLE);
|
||||
text.x = BUBBLE_PADDING;
|
||||
text.y = BUBBLE_PADDING;
|
||||
|
||||
const container = new PIXI.Container();
|
||||
container.addChild(bubble);
|
||||
container.addChild(text);
|
||||
container.bubble = bubble;
|
||||
container.text = text;
|
||||
|
||||
spriteData.currentBubble = container;
|
||||
stage.addChild(container);
|
||||
}
|
||||
|
||||
const container = spriteData.currentBubble;
|
||||
const { bubble, text } = container;
|
||||
|
||||
if (spriteData.sayTimeout !== null) {
|
||||
clearTimeout(spriteData.sayTimeout);
|
||||
spriteData.sayTimeout = null;
|
||||
}
|
||||
|
||||
if (text.text !== message) {
|
||||
text.text = message;
|
||||
|
||||
const bubbleWidth = text.width + BUBBLE_PADDING * 2;
|
||||
const bubbleHeight = text.height + BUBBLE_PADDING * 2;
|
||||
|
||||
bubble.clear();
|
||||
bubble.beginFill(BUBBLE_COLOR);
|
||||
bubble.lineStyle(2, LINE_COLOR);
|
||||
bubble.drawRoundedRect(0, 0, bubbleWidth, bubbleHeight, 10);
|
||||
|
||||
bubble.moveTo(bubbleWidth / 2 - BUBBLE_TAIL_WIDTH / 2, bubbleHeight);
|
||||
bubble.lineTo(bubbleWidth / 2, bubbleHeight + BUBBLE_TAIL_HEIGHT);
|
||||
bubble.lineTo(bubbleWidth / 2 + BUBBLE_TAIL_WIDTH / 2, bubbleHeight);
|
||||
bubble.closePath();
|
||||
bubble.endFill();
|
||||
}
|
||||
|
||||
const pos = calculateBubblePosition(
|
||||
sprite,
|
||||
bubble.width,
|
||||
bubble.height,
|
||||
BUBBLE_TAIL_HEIGHT
|
||||
);
|
||||
container.x = pos.x;
|
||||
container.y = pos.y;
|
||||
|
||||
container.visible = true;
|
||||
|
||||
if (typeof seconds === "number" && seconds > 0) {
|
||||
spriteData.sayTimeout = setTimeout(() => {
|
||||
container.visible = false;
|
||||
spriteData.sayTimeout = null;
|
||||
}, Math.min(seconds * 1000, 2147483647));
|
||||
}
|
||||
}
|
||||
|
||||
function waitOneFrame() {
|
||||
return new Promise((res, rej) => {
|
||||
if (stopped()) return rej("stopped");
|
||||
|
||||
const id = requestAnimationFrame(() => {
|
||||
if (stopped()) return rej("stopped");
|
||||
runningScripts.splice(
|
||||
runningScripts.findIndex((t) => t.id === id),
|
||||
1
|
||||
);
|
||||
res();
|
||||
});
|
||||
runningScripts.push({ type: "raf", id });
|
||||
});
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((res, rej) => {
|
||||
if (stopped()) return rej("stopped");
|
||||
|
||||
const id = setTimeout(() => {
|
||||
if (stopped()) return rej("stopped");
|
||||
runningScripts.splice(
|
||||
runningScripts.findIndex((t) => t.id === id),
|
||||
1
|
||||
);
|
||||
res();
|
||||
}, ms);
|
||||
runningScripts.push({ type: "timeout", id });
|
||||
});
|
||||
}
|
||||
|
||||
function switchCostume(name) {
|
||||
const found = costumeMap.get(name);
|
||||
if (found) {
|
||||
sprite.texture = found.texture;
|
||||
}
|
||||
}
|
||||
|
||||
function setSize(amount = 0, additive) {
|
||||
let amountN = amount / 100;
|
||||
if (additive)
|
||||
sprite.scale.set(sprite.scale.x + amountN, sprite.scale.y + amountN);
|
||||
else sprite.scale.set(amountN, amountN);
|
||||
}
|
||||
|
||||
function setAngle(amount, additive) {
|
||||
let angle = additive ? sprite.angle + amount : amount
|
||||
angle = ((angle % 360) + 360) % 360;
|
||||
sprite.angle = angle;
|
||||
}
|
||||
|
||||
function pointsTowards(x, y) {
|
||||
const { width, height } = renderer
|
||||
const targetX = width / 2 + x * stage.scale.x;
|
||||
const targetY = height / 2 - y * stage.scale.y;
|
||||
const spriteX = width / 2 + sprite.x * stage.scale.x;
|
||||
const spriteY = height / 2 - sprite.y * stage.scale.y;
|
||||
|
||||
let angle = Math.atan2(targetX - spriteX, targetY - spriteY) * (180 / Math.PI);
|
||||
angle = ((angle % 360) + 360) % 360;
|
||||
sprite.angle = angle;
|
||||
}
|
||||
|
||||
function projectTime() {
|
||||
return (Date.now() - projectStartedTime) / 1000;
|
||||
}
|
||||
|
||||
function isKeyPressed(key) {
|
||||
if (key === "any") {
|
||||
return Object.values(keysPressed).some((pressed) => pressed);
|
||||
}
|
||||
|
||||
return !!keysPressed[key];
|
||||
}
|
||||
|
||||
function isMouseButtonPressed(button) {
|
||||
if (button === "any") {
|
||||
return Object.values(mouseButtonsPressed).some((pressed) => pressed);
|
||||
}
|
||||
|
||||
return !!mouseButtonsPressed[button];
|
||||
}
|
||||
|
||||
function getCostumeSize(type) {
|
||||
const frame = sprite?.texture?.frame;
|
||||
if (!frame) return 0;
|
||||
|
||||
if (type === "width") return frame.width;
|
||||
else if (type === "height") return frame.height;
|
||||
else return 0;
|
||||
}
|
||||
|
||||
function getSpriteScale() {
|
||||
const scaleX = sprite.scale.x;
|
||||
const scaleY = sprite.scale.y;
|
||||
return ((scaleX + scaleY) / 2) * 100;
|
||||
}
|
||||
|
||||
function startTween({ from, to, duration, easing, onUpdate, wait = true }) {
|
||||
if (stopped()) return;
|
||||
|
||||
const tweenPromise = new Promise((resolve) => {
|
||||
const start = performance.now();
|
||||
const change = to - from;
|
||||
const easeFn = window.TweenEasing[easing] || window.TweenEasing.linear;
|
||||
|
||||
function tick(now) {
|
||||
if (stopped()) return resolve("shouldStop");
|
||||
|
||||
const t = Math.min((now - start) / (duration * 1000), 1);
|
||||
const value = from + change * easeFn(t);
|
||||
|
||||
if (onUpdate) {
|
||||
const result = onUpdate(value);
|
||||
if (result === "shouldStop") return resolve("shouldStop");
|
||||
}
|
||||
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
const id = requestAnimationFrame(tick);
|
||||
runningScripts.push({ type: "raf", id });
|
||||
});
|
||||
|
||||
return wait ? tweenPromise : undefined;
|
||||
}
|
||||
|
||||
let soundProperties = {
|
||||
volume: 100,
|
||||
speed: 100,
|
||||
};
|
||||
|
||||
function setSoundProperty(property, value) {
|
||||
if (!soundProperties[property]) return;
|
||||
if (property === "speed") value = Math.min(1600, Math.max(7, value));
|
||||
if (property === "volume") value = Math.min(100, Math.max(0, value));
|
||||
soundProperties[property] = value;
|
||||
}
|
||||
|
||||
async function playSound(name, wait = false) {
|
||||
const sound = soundMap.get(name);
|
||||
if (!sound) return;
|
||||
|
||||
if (!playingSounds.has(spriteData.id))
|
||||
playingSounds.set(spriteData.id, new Map());
|
||||
|
||||
const spriteSounds = playingSounds.get(spriteData.id);
|
||||
|
||||
const oldAudio = spriteSounds.get(name);
|
||||
if (oldAudio) {
|
||||
oldAudio.pause();
|
||||
oldAudio.currentTime = 0;
|
||||
}
|
||||
|
||||
const audio = new Audio(sound.dataURL);
|
||||
spriteSounds.set(name, audio);
|
||||
|
||||
audio.volume = soundProperties.volume / 100;
|
||||
audio.playbackRate = soundProperties.speed / 100;
|
||||
audio.play();
|
||||
|
||||
const cleanup = () => {
|
||||
if (spriteSounds.get(name) === audio) {
|
||||
spriteSounds.delete(name);
|
||||
}
|
||||
};
|
||||
|
||||
audio.addEventListener("ended", cleanup);
|
||||
audio.addEventListener("pause", cleanup);
|
||||
|
||||
if (wait) {
|
||||
return new Promise((res) => {
|
||||
audio.addEventListener("ended", () => res());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function stopSound(name) {
|
||||
const spriteSounds = playingSounds.get(spriteData.id);
|
||||
if (!spriteSounds || !spriteSounds.has(name)) return;
|
||||
|
||||
const audio = spriteSounds.get(name);
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
spriteSounds.delete(name);
|
||||
}
|
||||
|
||||
function stopAllSounds(thisSprite = false) {
|
||||
if (thisSprite) {
|
||||
const spriteSounds = playingSounds.get(spriteData.id);
|
||||
if (!spriteSounds) return;
|
||||
|
||||
for (const audio of spriteSounds.values()) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
|
||||
playingSounds.delete(spriteData.id);
|
||||
} else {
|
||||
for (const spriteSounds of playingSounds.values()) {
|
||||
for (const audio of spriteSounds.values()) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
}
|
||||
playingSounds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function isMouseTouchingSprite() {
|
||||
const mouse = renderer.events.pointer.global;
|
||||
const bounds = sprite.getBounds();
|
||||
return bounds.contains(mouse.x, mouse.y);
|
||||
}
|
||||
|
||||
function setPenStatus(active) {
|
||||
spriteData.penDown = !!active;
|
||||
}
|
||||
|
||||
function setPenColor(r, g, b) {
|
||||
if (typeof r === "string") {
|
||||
const [r_, g_, b_] = r.split(",");
|
||||
r = +r_;
|
||||
g = +g_;
|
||||
b = +b_;
|
||||
}
|
||||
spriteData.penColor = (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
function setPenColorHex(value) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(value);
|
||||
spriteData.penColor = result
|
||||
? (parseInt(result[1], 16) << 16) |
|
||||
(parseInt(result[2], 16) << 8) |
|
||||
parseInt(result[3], 16)
|
||||
: 0x000000;
|
||||
}
|
||||
|
||||
function setPenSize(size = 0) {
|
||||
spriteData.penSize = Math.max(1, size);
|
||||
}
|
||||
|
||||
function clearPen() {
|
||||
penGraphics.clear();
|
||||
}
|
||||
|
||||
function toggleVisibility(bool = true) {
|
||||
sprite.visible = bool;
|
||||
if (spriteData.currentBubble) spriteData.currentBubble.visible = bool;
|
||||
}
|
||||
|
||||
eval(code);
|
||||
}
|
||||
282
src/functions/theme.js
Normal file
@@ -0,0 +1,282 @@
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import * as Blockly from "blockly";
|
||||
import config from "../config";
|
||||
import { cache } from "../cache";
|
||||
import { showPopup } from "./utils";
|
||||
|
||||
const root = document.documentElement;
|
||||
const theme = localStorage.getItem("theme") === "dark" ?? false;
|
||||
const icons = localStorage.getItem("removeIcons") === "true" ?? false;
|
||||
const rarryToolbar =
|
||||
localStorage.getItem("removeRarryToolbar") === "true" ?? false;
|
||||
const toolboxPosition =
|
||||
localStorage.getItem("toolboxPosition") || "space-between";
|
||||
const headerColor = localStorage.getItem("headerColor") || "";
|
||||
const stageLeft = localStorage.getItem("stageLeft") === "true" ?? false;
|
||||
|
||||
const blockStyles = {
|
||||
logic_blocks: {
|
||||
colourPrimary: "#59BA57",
|
||||
},
|
||||
math_blocks: {
|
||||
colourPrimary: "#59BA57",
|
||||
},
|
||||
text_blocks: {
|
||||
colourPrimary: "#59BA57",
|
||||
},
|
||||
loop_blocks: {
|
||||
colourPrimary: "#FFAB19",
|
||||
},
|
||||
variable_blocks: {
|
||||
colourPrimary: "#FF8C1A",
|
||||
},
|
||||
list_blocks: {
|
||||
colourPrimary: "#E35340",
|
||||
},
|
||||
procedure_blocks: {
|
||||
colourPrimary: "#FF6680",
|
||||
},
|
||||
motion_blocks: {
|
||||
colourPrimary: "#4C97FF",
|
||||
},
|
||||
looks_blocks: {
|
||||
colourPrimary: "#9966FF",
|
||||
},
|
||||
events_blocks: {
|
||||
colourPrimary: "#e9c600",
|
||||
},
|
||||
control_blocks: {
|
||||
colourPrimary: "#FFAB19",
|
||||
},
|
||||
json_category: {
|
||||
colourPrimary: "#FF8349",
|
||||
},
|
||||
set_blocks: {
|
||||
colourPrimary: "#2CC2A9",
|
||||
},
|
||||
};
|
||||
|
||||
const lightTheme = Blockly.Theme.defineTheme("customLightTheme", {
|
||||
base: Blockly.Themes.Classic,
|
||||
blockStyles: blockStyles,
|
||||
});
|
||||
|
||||
const darkTheme = Blockly.Theme.defineTheme("customDarkTheme", {
|
||||
base: Blockly.Themes.Classic,
|
||||
blockStyles: blockStyles,
|
||||
componentStyles: {
|
||||
workspaceBackgroundColour: "#1a1e25",
|
||||
toolboxBackgroundColour: "#303236",
|
||||
toolboxForegroundColour: "#fff",
|
||||
flyoutBackgroundColour: "#212327",
|
||||
flyoutForegroundColour: "#ccc",
|
||||
flyoutOpacity: 1,
|
||||
scrollbarColour: "#797979",
|
||||
insertionMarkerColour: "#fff",
|
||||
insertionMarkerOpacity: 0.3,
|
||||
scrollbarOpacity: 0.4,
|
||||
cursorColour: "#d0d0d0",
|
||||
},
|
||||
});
|
||||
|
||||
export function toggleTheme(dark, workspace) {
|
||||
localStorage.setItem("theme", dark ? "dark" : "light");
|
||||
|
||||
if (dark) root.classList.add("dark");
|
||||
else root.classList.remove("dark");
|
||||
|
||||
if (workspace) workspace.setTheme(dark ? darkTheme : lightTheme);
|
||||
}
|
||||
|
||||
export function toggleIcons(removeIcons) {
|
||||
localStorage.setItem("removeIcons", String(removeIcons));
|
||||
|
||||
if (removeIcons) root.classList.add("removeIcons");
|
||||
else root.classList.remove("removeIcons");
|
||||
}
|
||||
|
||||
export function toggleRarryToolbar(removeIcon) {
|
||||
localStorage.setItem("removeRarryToolbar", String(removeIcon));
|
||||
|
||||
if (removeIcon) root.classList.add("removeRarryToolbar");
|
||||
else root.classList.remove("removeRarryToolbar");
|
||||
}
|
||||
|
||||
export function setToolboxPosition(pos) {
|
||||
localStorage.setItem("toolboxPosition", pos);
|
||||
|
||||
const header = document.querySelector("header");
|
||||
if (!header) return;
|
||||
|
||||
root.classList.remove("toolbox-left", "toolbox-center", "toolbox-right");
|
||||
|
||||
if (pos === "default") return;
|
||||
|
||||
root.classList.add(`toolbox-${pos}`);
|
||||
}
|
||||
|
||||
export function setHeaderColor(color) {
|
||||
localStorage.setItem("headerColor", color);
|
||||
|
||||
if (!color) root.style.removeProperty("--header-color");
|
||||
else root.style.setProperty("--header-color", color);
|
||||
}
|
||||
|
||||
export function toggleStageLeft(left) {
|
||||
localStorage.setItem("stageLeft", String(left));
|
||||
|
||||
if (left) root.classList.add("stageLeft");
|
||||
else root.classList.remove("stageLeft");
|
||||
}
|
||||
|
||||
export function setupThemeButton(workspace) {
|
||||
toggleTheme(theme, workspace);
|
||||
toggleIcons(icons);
|
||||
toggleRarryToolbar(rarryToolbar);
|
||||
toggleStageLeft(stageLeft);
|
||||
setToolboxPosition(toolboxPosition);
|
||||
setHeaderColor(headerColor);
|
||||
|
||||
const themeButton = document.getElementById("theme-button");
|
||||
if (themeButton)
|
||||
themeButton.addEventListener("click", () =>
|
||||
showPopup({
|
||||
title: "Appearance",
|
||||
rows: [
|
||||
[
|
||||
"Theme:",
|
||||
{
|
||||
type: "button",
|
||||
label: '<i class="fa-solid fa-sun"></i> Light',
|
||||
onClick: () => toggleTheme(false, workspace),
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: '<i class="fa-solid fa-moon"></i> Dark',
|
||||
onClick: () => toggleTheme(true, workspace),
|
||||
},
|
||||
],
|
||||
[
|
||||
"Show icon on buttons:",
|
||||
{
|
||||
type: "checkbox",
|
||||
checked:
|
||||
!document.documentElement.classList.contains("removeIcons"),
|
||||
onChange: checked => {
|
||||
toggleIcons(!checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Show Rarry logo on toolbar:",
|
||||
{
|
||||
type: "checkbox",
|
||||
checked:
|
||||
!document.documentElement.classList.contains(
|
||||
"removeRarryToolbar"
|
||||
),
|
||||
onChange: checked => {
|
||||
toggleRarryToolbar(!checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"Toolbar color:",
|
||||
{
|
||||
type: "color",
|
||||
value: localStorage.getItem("headerColor") || "",
|
||||
onChange: value => setHeaderColor(value),
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Reset",
|
||||
onClick: () => setHeaderColor(""),
|
||||
},
|
||||
],
|
||||
[
|
||||
"Toolbar position:",
|
||||
{
|
||||
type: "menu",
|
||||
value: localStorage.getItem("toolboxPosition") || "default",
|
||||
options: [
|
||||
{ label: "Space Between (default)", value: "default" },
|
||||
{ label: "Left", value: "left" },
|
||||
{ label: "Center", value: "center" },
|
||||
{ label: "Right", value: "right" },
|
||||
],
|
||||
onChange: value => setToolboxPosition(value),
|
||||
},
|
||||
],
|
||||
...(workspace
|
||||
? [
|
||||
[
|
||||
"Renderer (applies after refresh):",
|
||||
{
|
||||
type: "menu",
|
||||
value: localStorage.getItem("renderer"),
|
||||
options: [
|
||||
{ label: "Zelos (default)", value: "custom_zelos" },
|
||||
{ label: "Thrasos", value: "thrasos" },
|
||||
{ label: "Geras", value: "geras" },
|
||||
],
|
||||
onChange: value => localStorage.setItem("renderer", value),
|
||||
},
|
||||
],
|
||||
[
|
||||
"Stage on left:",
|
||||
{
|
||||
type: "checkbox",
|
||||
checked:
|
||||
document.documentElement.classList.contains("stageLeft"),
|
||||
onChange: checked => {
|
||||
toggleStageLeft(checked);
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
: []),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function setupUserTag() {
|
||||
function setUserTag(user) {
|
||||
if (user === null) {
|
||||
if (cache.user === null) return;
|
||||
user = cache.user;
|
||||
}
|
||||
|
||||
login.parentElement.innerHTML = `
|
||||
<div class="userTag">
|
||||
<img src="${config.apiUrl}/users/${user.id}/avatar" />
|
||||
<a href="/user?id=${user.id}">${user.username}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const login = document.getElementById("login-button");
|
||||
if (login && localStorage.getItem("tooken") !== null) {
|
||||
if (cache.user) {
|
||||
setUserTag(cache.user);
|
||||
} else {
|
||||
fetch(`${config.apiUrl}/users/me`, {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem("tooken"),
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
"Failed to fetch user data: " + response.statusText
|
||||
);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
cache.user = data;
|
||||
setUserTag(data);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/functions/threads.js
Normal file
@@ -0,0 +1,84 @@
|
||||
export function createThreadSystem() {
|
||||
const threads = new Map();
|
||||
let current = null;
|
||||
let nextId = 1;
|
||||
|
||||
function ensure(threadId) {
|
||||
if (!threads.has(threadId)) threads.set(threadId, {});
|
||||
}
|
||||
|
||||
return {
|
||||
resetAll() {
|
||||
threads.clear();
|
||||
current = null;
|
||||
nextId = 1;
|
||||
},
|
||||
|
||||
create(initialVars = {}) {
|
||||
const id = `t${nextId++}`;
|
||||
threads.set(id, { ...initialVars });
|
||||
return id;
|
||||
},
|
||||
|
||||
enter(threadId) {
|
||||
ensure(threadId);
|
||||
current = threadId;
|
||||
return this.getCurrentContext();
|
||||
},
|
||||
|
||||
exit() {
|
||||
current = null;
|
||||
},
|
||||
|
||||
set(threadId, key, value) {
|
||||
ensure(threadId);
|
||||
threads.get(threadId)[key] = value;
|
||||
},
|
||||
get(threadId, key) {
|
||||
return threads.get(threadId) ? threads.get(threadId)[key] : undefined;
|
||||
},
|
||||
has(threadId, key) {
|
||||
return threads.get(threadId)
|
||||
? threads.get(threadId)[key] !== undefined
|
||||
: false;
|
||||
},
|
||||
|
||||
getCurrentContext() {
|
||||
const threadId = current;
|
||||
return {
|
||||
id: threadId,
|
||||
vars: threadId ? threads.get(threadId) : null,
|
||||
set: (k, v) => {
|
||||
if (!threadId) return;
|
||||
threads.get(threadId)[k] = v;
|
||||
},
|
||||
get: (k) => {
|
||||
if (!threadId) return undefined;
|
||||
return threads.get(threadId)[k];
|
||||
},
|
||||
has: (k) => {
|
||||
if (!threadId) return false;
|
||||
return threads.get(threadId)[k] !== undefined;
|
||||
},
|
||||
spawn: (fn, initialVars = {}) => {
|
||||
const newId = `t${nextId++}`;
|
||||
threads.set(newId, { ...initialVars });
|
||||
|
||||
const prev = current;
|
||||
current = newId;
|
||||
try {
|
||||
return fn({
|
||||
id: newId,
|
||||
get: (k) => threads.get(newId)[k],
|
||||
set: (k, v) => (threads.get(newId)[k] = v),
|
||||
});
|
||||
} finally {
|
||||
current = prev;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const Thread = createThreadSystem();
|
||||
260
src/functions/utils.js
Normal file
@@ -0,0 +1,260 @@
|
||||
let currentPopup;
|
||||
|
||||
export function showNotification({
|
||||
message = "",
|
||||
duration = 5000,
|
||||
closable = true,
|
||||
}) {
|
||||
const notification = document.createElement("div");
|
||||
notification.className = "notification";
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
${
|
||||
closable
|
||||
? '<button class="notification-close"><i class="fa-solid fa-xmark"></i></button>'
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
let container = document.querySelector(".notification-container");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "notification-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
function hide() {
|
||||
notification.classList.add("hide");
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}
|
||||
|
||||
if (closable) {
|
||||
notification
|
||||
.querySelector(".notification-close")
|
||||
?.addEventListener("click", hide);
|
||||
}
|
||||
|
||||
setTimeout(hide, duration);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
export function showPopup({ innerHTML = "", title = "", rows = [] }) {
|
||||
const popup = document.createElement("div");
|
||||
popup.className = "popup";
|
||||
|
||||
if (currentPopup) currentPopup.remove();
|
||||
currentPopup = popup;
|
||||
|
||||
const rowsHTML = rows
|
||||
.map((row, rowIndex) => {
|
||||
const rowHTML = row
|
||||
.map((item, colIndex) => {
|
||||
if (typeof item === "string") {
|
||||
if (item === "") return;
|
||||
return `<span class="popup-label">${item}</span>`;
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case "custom":
|
||||
return item.html || "";
|
||||
case "button":
|
||||
return `<button
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
${item.disabled ? "disabled" : ""}
|
||||
>${item.label}</button>`;
|
||||
case "input":
|
||||
return `<input
|
||||
type="${item.inputType || "text"}"
|
||||
placeholder="${item.placeholder || ""}"
|
||||
value="${item.value || ""}"
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
/>`;
|
||||
case "checkbox":
|
||||
return `<input
|
||||
type="checkbox"
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
${item.checked ? "checked" : ""}
|
||||
/>`;
|
||||
case "textarea":
|
||||
return `<textarea
|
||||
placeholder="${item.placeholder || ""}"
|
||||
rows="${item.rows || 3}"
|
||||
cols="${item.cols || 30}"
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
>${item.value || ""}</textarea>`;
|
||||
case "label":
|
||||
return `<span class="popup-label">${item.text}</span>`;
|
||||
case "menu":
|
||||
return `<select
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
>
|
||||
${item.options
|
||||
.map(
|
||||
opt =>
|
||||
`<option value="${opt.value}" ${
|
||||
opt.value === item.value ? "selected" : ""
|
||||
}>${opt.label}</option>`
|
||||
)
|
||||
.join("")}
|
||||
</select>`;
|
||||
case "color":
|
||||
return `<input
|
||||
type="color"
|
||||
value="${item.value || "#ffffff"}"
|
||||
class="${item.className || ""}"
|
||||
data-row="${rowIndex}" data-col="${colIndex}"
|
||||
/>`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
return `<div class="popup-row">${rowHTML}</div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
popup.innerHTML = `
|
||||
<div class="popup-content">
|
||||
<header>
|
||||
<h2>${title}</h2>
|
||||
<button class="popup-close danger"><i class="fa-solid fa-xmark stay"></i></button>
|
||||
</header>
|
||||
<div class="popup-body">
|
||||
${rowsHTML}
|
||||
${innerHTML}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
popup.querySelector(".popup-close").addEventListener("click", () => {
|
||||
currentPopup = null;
|
||||
popup.remove();
|
||||
});
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
row.forEach((item, colIndex) => {
|
||||
const el = popup.querySelector(
|
||||
`[data-row="${rowIndex}"][data-col="${colIndex}"]`
|
||||
);
|
||||
if (!el) return;
|
||||
|
||||
if (item.type === "button" && item.onClick) {
|
||||
el.addEventListener("click", () => item.onClick(popup));
|
||||
}
|
||||
if (item.type === "input" && item.onInput) {
|
||||
el.addEventListener("input", e => item.onInput(e.target.value, popup));
|
||||
}
|
||||
if (item.type === "checkbox" && item.onChange) {
|
||||
el.addEventListener("change", e =>
|
||||
item.onChange(e.target.checked, popup)
|
||||
);
|
||||
}
|
||||
if (item.type === "textarea" && item.onInput) {
|
||||
el.addEventListener("input", e => item.onInput(e.target.value, popup));
|
||||
}
|
||||
if (item.type === "menu" && item.onChange) {
|
||||
el.addEventListener("change", e =>
|
||||
item.onChange(e.target.value, popup)
|
||||
);
|
||||
}
|
||||
if (item.type === "color" && item.onChange) {
|
||||
el.addEventListener("input", e => item.onChange(e.target.value, popup));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
export function promiseWithAbort(promiseOrFn, signal) {
|
||||
try {
|
||||
const p = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
|
||||
if (!(p instanceof Promise)) return Promise.resolve(p);
|
||||
|
||||
if (signal.aborted) return Promise.reject(new Error("shouldStop"));
|
||||
|
||||
return Promise.race([
|
||||
p,
|
||||
new Promise((_, rej) => {
|
||||
signal.addEventListener("abort", () => rej(new Error("shouldStop")), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function encodeOggWithMediaRecorder(dataURL) {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
const base64 = dataURL.split(",")[1];
|
||||
const raw = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
||||
const buffer = await audioCtx.decodeAudioData(raw.buffer);
|
||||
|
||||
const src = audioCtx.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
|
||||
const dest = audioCtx.createMediaStreamDestination();
|
||||
src.connect(dest);
|
||||
|
||||
const recorder = new MediaRecorder(dest.stream, {
|
||||
mimeType: "audio/ogg",
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
recorder.ondataavailable = e => chunks.push(e.data);
|
||||
|
||||
return new Promise(resolve => {
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(chunks, { type: "audio/ogg" });
|
||||
const fr = new FileReader();
|
||||
fr.onloadend = () => resolve(fr.result);
|
||||
fr.readAsDataURL(blob);
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
src.start();
|
||||
src.onended = () => recorder.stop();
|
||||
});
|
||||
}
|
||||
|
||||
export async function compressAudio(dataURL) {
|
||||
if (window.MediaRecorder && MediaRecorder.isTypeSupported("audio/ogg")) {
|
||||
try {
|
||||
return await encodeOggWithMediaRecorder(dataURL);
|
||||
} catch (e) {
|
||||
console.warn("OGG recording failed, falling back:", e);
|
||||
}
|
||||
}
|
||||
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
export async function compressImage(dataURL) {
|
||||
if (!dataURL || typeof dataURL !== "string") return null;
|
||||
if (dataURL.startsWith("data:image/webp")) return dataURL;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
resolve(canvas.toDataURL("image/webp", 0.9));
|
||||
};
|
||||
img.src = dataURL;
|
||||
});
|
||||
}
|
||||
70
src/home.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 3rem;
|
||||
background-image: url("/icons/blur.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
text-align: center;
|
||||
color: #f3f4f6;
|
||||
font-size: larger;
|
||||
text-shadow: 0 2px 3px black;
|
||||
}
|
||||
|
||||
.about * {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.feature-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background-color: var(--color1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
max-width: 15rem;
|
||||
text-shadow: none;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 1.25rem;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--dark-light);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cta-section h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
button.large {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
489
src/index.css
Normal file
@@ -0,0 +1,489 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||
|
||||
html {
|
||||
--font: "Inter", sans-serif;
|
||||
--dark: #2b323b;
|
||||
--dark-light: #424c5a;
|
||||
--primary: #833bf6;
|
||||
--primary-dark: #8930dc;
|
||||
--danger: #f63b3b;
|
||||
--danger-dark: #dd3434;
|
||||
--color1: #f3f4f6;
|
||||
--color2: #e4e5e7;
|
||||
--color3: #cbcdcf;
|
||||
--color4: #b9bbbd;
|
||||
--header-color: var(--primary);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--dark: #e2e8f0;
|
||||
--dark-light: #c8cdd4;
|
||||
--primary: #833bf6;
|
||||
--primary-dark: #8930dc;
|
||||
--danger: #f63b3b;
|
||||
--danger-dark: #dd3434;
|
||||
--color1: #262d36;
|
||||
--color2: #2f3741;
|
||||
--color3: #3d4552;
|
||||
--color4: #464f5e;
|
||||
--header-color: var(--color4);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
input {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
body,
|
||||
div#app {
|
||||
font-family: var(--font);
|
||||
background-color: var(--color1);
|
||||
overscroll-behavior: none;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
div#app,
|
||||
div#stage {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--header-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.3rem 0.5rem;
|
||||
z-index: 10;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
html.toolbox-left header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
html.toolbox-center header {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
html.toolbox-right header {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
header h2,
|
||||
header button:not(.white) {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
header div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hidden,
|
||||
html.removeRarryToolbar header img.logo {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="file"] {
|
||||
font-family: var(--font);
|
||||
font-size: medium;
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
color: var(--color1);
|
||||
cursor: pointer;
|
||||
will-change: background-color, scale;
|
||||
transition: background-color 0.1s, scale 0.1s;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
select,
|
||||
textarea:not(.blocklyTextarea) {
|
||||
font-family: var(--font);
|
||||
font-size: medium;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color2);
|
||||
border: 1px solid var(--color4);
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
background-color: var(--dark);
|
||||
}
|
||||
|
||||
button:not(.tab-button):active {
|
||||
scale: 0.95;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button {
|
||||
font-family: var(--font);
|
||||
background-color: white;
|
||||
color: #2b323b;
|
||||
border: none;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
img.button {
|
||||
width: 2rem;
|
||||
cursor: pointer;
|
||||
will-change: scale;
|
||||
transition: scale 0.1s;
|
||||
}
|
||||
|
||||
img.button:active {
|
||||
scale: 0.95;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--dark-light);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background-color: var(--danger-dark);
|
||||
}
|
||||
|
||||
button.orange {
|
||||
background-color: #f18f3f;
|
||||
}
|
||||
|
||||
button.orange:hover {
|
||||
background-color: #e28940;
|
||||
}
|
||||
|
||||
button.green {
|
||||
background-color: #2acc66;
|
||||
}
|
||||
|
||||
button.green:hover {
|
||||
background-color: #28b85c;
|
||||
}
|
||||
|
||||
button.large {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
html.dark header button {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
html:not(.removeIcons) button:has(i:not(.stay)) {
|
||||
padding-left: 0.7rem;
|
||||
}
|
||||
|
||||
html.removeIcons button i:not(.stay) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
scale: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
position: relative;
|
||||
border-radius: 0.5rem;
|
||||
width: max(300px, 50%);
|
||||
height: max(300px, 50%);
|
||||
background: var(--color2);
|
||||
color: var(--dark);
|
||||
animation: scaleIn 0.3s ease;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.popup-content header {
|
||||
padding: 0.5rem;
|
||||
padding-left: 0.7rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.popup-body textarea.extension-code-input {
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popup-body,
|
||||
.popup-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
margin-left: auto;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: var(--color2);
|
||||
color: var(--dark);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||
padding: 0.4rem;
|
||||
padding-left: 0.8rem;
|
||||
animation: scaleIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.notification.hide {
|
||||
animation: scaleOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
background: none;
|
||||
color: var(--dark);
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.notification-close:hover {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: var(--color3);
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin: 0;
|
||||
padding: 0.7rem;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
height: 9px;
|
||||
border: solid #f3f4f6;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
background: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.userTag {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--primary-dark);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.userTag a {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
html.dark .userTag {
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
.userTag img {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
background-color: var(--color2);
|
||||
color: var(--dark-light);
|
||||
font-size: 0.9rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.dropdown-content button:hover {
|
||||
background-color: var(--dark-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color4);
|
||||
}
|
||||
|
||||
:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.project-name-input {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
min-width: 220px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.project-name-input:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.project-name-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
background: var(--background-secondary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.project-name-input::placeholder {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.project-name-input::before {
|
||||
content: "📝";
|
||||
margin-right: 8px;
|
||||
}
|
||||
24
src/login.css
Normal file
@@ -0,0 +1,24 @@
|
||||
body {
|
||||
background-image: url("/icons/blur.png");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
body,
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-radius: 1rem;
|
||||
gap: 1.5rem;
|
||||
padding: 2rem;
|
||||
font-size: large;
|
||||
background-color: var(--color1);
|
||||
color: var(--dark);
|
||||
}
|
||||
2743
src/scripts/editor.js
Normal file
4
src/scripts/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupThemeButton, setupUserTag } from "../functions/theme";
|
||||
|
||||
setupThemeButton();
|
||||
setupUserTag();
|
||||
59
src/scripts/login.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import { toggleIcons, toggleTheme } from "../functions/theme";
|
||||
import config from "../config";
|
||||
|
||||
toggleTheme();
|
||||
toggleIcons();
|
||||
|
||||
const usernameInput = document.getElementById("username");
|
||||
const passwordInput = document.getElementById("password");
|
||||
const loginButton = document.getElementById("login");
|
||||
|
||||
async function onLoginClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
alert("Please enter both username and password.");
|
||||
return;
|
||||
}
|
||||
|
||||
loginButton.disabled = true;
|
||||
loginButton.dataset.origHtml = loginButton.innerHTML;
|
||||
loginButton.innerHTML = `...`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/users/login`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errText = `Request failed: ${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const errJson = await response.json();
|
||||
|
||||
errText = errJson.message || JSON.stringify(errJson);
|
||||
} catch (err) {}
|
||||
throw new Error(errText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.token) localStorage.setItem("tooken", data.token);
|
||||
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Login error: " + err.message);
|
||||
|
||||
loginButton.disabled = false;
|
||||
loginButton.innerHTML = loginButton.dataset.origHtml;
|
||||
}
|
||||
}
|
||||
|
||||
loginButton.addEventListener("click", onLoginClick);
|
||||
59
src/scripts/signup.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import { toggleIcons, toggleTheme } from "../functions/theme";
|
||||
import config from "../config";
|
||||
|
||||
toggleTheme();
|
||||
toggleIcons();
|
||||
|
||||
const usernameInput = document.getElementById("username");
|
||||
const passwordInput = document.getElementById("password");
|
||||
const loginButton = document.getElementById("login");
|
||||
|
||||
async function onSignupClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
alert("Please enter both username and password.");
|
||||
return;
|
||||
}
|
||||
|
||||
loginButton.disabled = true;
|
||||
loginButton.dataset.origHtml = loginButton.innerHTML;
|
||||
loginButton.innerHTML = `...`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/users/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errText = `Request failed: ${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const errJson = await response.json();
|
||||
|
||||
errText = errJson.message || JSON.stringify(errJson);
|
||||
} catch (err) {}
|
||||
throw new Error(errText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.token) localStorage.setItem("tooken", data.token);
|
||||
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Login error: " + err.message);
|
||||
|
||||
loginButton.disabled = false;
|
||||
loginButton.innerHTML = loginButton.dataset.origHtml;
|
||||
}
|
||||
}
|
||||
|
||||
loginButton.addEventListener("click", onSignupClick);
|
||||
128
src/scripts/testExt.js
Normal file
@@ -0,0 +1,128 @@
|
||||
class Extension {
|
||||
id = "ddeTestExtension";
|
||||
registerCategory() {
|
||||
return {
|
||||
name: "Test Extension",
|
||||
color: "#858585",
|
||||
};
|
||||
}
|
||||
registerBlocks() {
|
||||
return [
|
||||
{
|
||||
type: "statement",
|
||||
id: "evil",
|
||||
text: "evil block",
|
||||
color: "#FF0000",
|
||||
},
|
||||
{
|
||||
type: "cap",
|
||||
id: "statement",
|
||||
fields: { poop: { kind: "statement" } },
|
||||
text: "i want statement [poop]",
|
||||
},
|
||||
{
|
||||
type: "statement",
|
||||
id: "statementA",
|
||||
text: "type statement A",
|
||||
statementType: "statementA",
|
||||
color: "#85c25c",
|
||||
},
|
||||
{
|
||||
type: "statement",
|
||||
id: "onlyStatementA",
|
||||
fields: { code: { kind: "statement", accepts: "statementA" } },
|
||||
text: "only statement A [code]",
|
||||
color: "#69974a",
|
||||
},
|
||||
{
|
||||
type: "statement",
|
||||
id: "if",
|
||||
fields: {
|
||||
bool: { kind: "value", type: "Boolean", default: true },
|
||||
code: { kind: "statement" },
|
||||
},
|
||||
text: "if [bool] then [code]",
|
||||
},
|
||||
{
|
||||
type: "statement",
|
||||
id: "ifElse",
|
||||
fields: {
|
||||
bool: { kind: "value", type: "Boolean", default: true },
|
||||
code: { kind: "statement" },
|
||||
codeElse: { kind: "statement" },
|
||||
},
|
||||
text: "if [bool] then [code] else [codeElse]",
|
||||
},
|
||||
{
|
||||
type: "statement",
|
||||
id: "menu",
|
||||
fields: {
|
||||
hi: {
|
||||
kind: "menu",
|
||||
items: ["normal", { text: "ABC display", value: "abc" }],
|
||||
default: "abc",
|
||||
},
|
||||
},
|
||||
text: "menu [hi]",
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
id: "random1",
|
||||
text: "random (output shape 1)",
|
||||
outputShape: 1,
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
id: "random2",
|
||||
text: "random (output shape 2)",
|
||||
outputShape: 2,
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
id: "random3",
|
||||
text: "random (output shape 3)",
|
||||
outputShape: 3,
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
id: "random4",
|
||||
text: "random (output shape 4)",
|
||||
outputShape: 4,
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
id: "random5",
|
||||
text: "random (output shape 5)",
|
||||
outputShape: 5,
|
||||
},
|
||||
];
|
||||
}
|
||||
registerCode() {
|
||||
return {
|
||||
statement: (inputs) => {
|
||||
console.log(inputs.poop?.());
|
||||
},
|
||||
if: (inputs) => {
|
||||
console.log(inputs);
|
||||
if (inputs.bool) inputs.code?.();
|
||||
},
|
||||
ifElse: (inputs) => {
|
||||
console.log(inputs);
|
||||
if (inputs.bool) inputs.code?.();
|
||||
else inputs.codeElse?.();
|
||||
},
|
||||
evil: () => {
|
||||
console.warn("evil is near");
|
||||
},
|
||||
random1: () => Math.random(),
|
||||
random2: () => Math.random(),
|
||||
random3: () => Math.random(),
|
||||
random4: () => Math.random(),
|
||||
random5: () => Math.random(),
|
||||
actuallyBoolean: () => true,
|
||||
menu: (inputs) => window.alert(inputs.hi),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerExtension(Extension);
|
||||
129
src/scripts/userprofile.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import config from "../config";
|
||||
import { cache } from "../cache";
|
||||
import { showPopup } from "../functions/utils";
|
||||
import { setupThemeButton, setupUserTag } from "../functions/theme";
|
||||
|
||||
const allowedFileFormats = ["jpeg", "png", "webp", "avif", "gif", "tiff"];
|
||||
const profileDiv = document.getElementById("userProfile");
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const identifier = urlParams.get("id") || urlParams.get("username");
|
||||
|
||||
setupThemeButton();
|
||||
setupUserTag();
|
||||
|
||||
if (profileDiv && identifier) {
|
||||
fetch(`${config.apiUrl}/users/${identifier}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
profileDiv.innerHTML = `
|
||||
<img src="${config.apiUrl}/users/${response.id}/avatar" id="profileAvatar" />
|
||||
${response.username}
|
||||
`;
|
||||
|
||||
if (cache.user && cache.user.id === response.id) {
|
||||
const avatarEl = document.getElementById("profileAvatar");
|
||||
avatarEl.style.cursor = "pointer";
|
||||
avatarEl.addEventListener("click", async () => {
|
||||
showPopup({
|
||||
title: "Avatar",
|
||||
rows: [
|
||||
[
|
||||
{
|
||||
type: "button",
|
||||
label: "Upload avatar",
|
||||
className: "primary",
|
||||
onClick: async (popup) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = allowedFileFormats
|
||||
.map((f) => `image/${f}`)
|
||||
.join(",");
|
||||
input.style.display = "none";
|
||||
input.click();
|
||||
|
||||
input.onchange = async () => {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const format = file.type.split("/")[1];
|
||||
if (!allowedFileFormats.includes(format)) {
|
||||
alert(`Unsupported format: ${format}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("avatar", file);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${config.apiUrl}/users/me/avatar`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: localStorage.getItem("tooken"),
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok)
|
||||
throw new Error(data.error || "Upload failed");
|
||||
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error uploading: " + err.message);
|
||||
} finally {
|
||||
popup.remove();
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
label: "Remove avatar",
|
||||
className: "danger",
|
||||
onClick: async (popup) => {
|
||||
if (!confirm("Remove your avatar?")) return;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${config.apiUrl}/users/me/avatar`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: localStorage.getItem("tooken"),
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
data.error || "Failed to remove avatar"
|
||||
);
|
||||
|
||||
avatarEl.src = "default-avatar.png";
|
||||
popup.remove();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error deleting: " + err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
alert("Login error: " + err.message);
|
||||
});
|
||||
}
|
||||
27
src/userprofile.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.info {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#userProfile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color2);
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
#userProfile img {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 20%;
|
||||
}
|
||||
61
user.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>User - NeoIDE</title>
|
||||
|
||||
<link href="src/userprofile.css" rel="stylesheet" />
|
||||
<link href="src/index.css" rel="stylesheet" />
|
||||
|
||||
<meta name="author" content="ddededodediamante" />
|
||||
<meta name="description" content="Create and share games using visual block-coding, inspired by Scratch." />
|
||||
<meta name="keywords"
|
||||
content="NeoIDE, block coding, Scratch, visual programming, game maker, coding for kids, online coding" />
|
||||
<meta name="theme-color" content="#3b82f6" />
|
||||
<meta property="og:title" content="NeoIDE" />
|
||||
<meta property="og:image" content="icons/NeoIDE.svg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="NeoIDE" />
|
||||
<meta name="twitter:description" content="Create and share games using visual block-coding, inspired by Scratch." />
|
||||
<meta name="twitter:image" content="icons/NeoIDE.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<img src="/icons/NeoIDE.svg" alt="NeoIDE logo" class="logo" onclick="location.href='/'" style="cursor: pointer" />
|
||||
<button onclick="location.href='/editor'">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
Editor
|
||||
</button>
|
||||
<button id="theme-button">
|
||||
<i class="fa-solid fa-paintbrush"></i>
|
||||
Appearance
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="location.href='/login'" id="login-button">
|
||||
<i class="fa-solid fa-right-to-bracket"></i>
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="info">
|
||||
<div id="userProfile"></div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
© 2025 NeoIDE. Made with ❤️ by
|
||||
<a href="https://github.com/ddededodediamante">ddededodediamante</a>.
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="src/scripts/userprofile.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
vercel.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"cleanUrls": true
|
||||
}
|
||||
17
vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'index.html'),
|
||||
editor: resolve(__dirname, 'editor.html'),
|
||||
login: resolve(__dirname, 'login.html'),
|
||||
signup: resolve(__dirname, 'signup.html'),
|
||||
user: resolve(__dirname, 'user.html'),
|
||||
},
|
||||
treeshake: false,
|
||||
},
|
||||
},
|
||||
});
|
||||