This commit is contained in:
2026-01-19 23:44:32 -06:00
commit 11bd5aa72b
64 changed files with 13433 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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>
&copy; 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
View 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

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

9
public/icons/NeoIDE.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 121 KiB

1
public/icons/R-Rarry.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

1
public/icons/flag.svg Normal file
View 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

View 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
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

1
public/icons/play.svg Normal file
View 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
View 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
View 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

View 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

View 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
View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
export const cache = {
user: null,
};

5
src/config.js Normal file
View 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
View 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;
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

4
src/scripts/index.js Normal file
View File

@@ -0,0 +1,4 @@
import { setupThemeButton, setupUserTag } from "../functions/theme";
setupThemeButton();
setupUserTag();

59
src/scripts/login.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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>
&copy; 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
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"cleanUrls": true
}

17
vite.config.js Normal file
View 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,
},
},
});