From c0c05b2251e184a356a0039052386a8ad9e8d521 Mon Sep 17 00:00:00 2001 From: SharkPool-SP <139097378+SharkPool-SP@users.noreply.github.com> Date: Sat, 2 Sep 2023 22:45:18 -0700 Subject: [PATCH 01/26] Add Image-Effects --- extensions/SharkPool/Image-Effects | 1285 ++++++++++++++++++++++++++++ 1 file changed, 1285 insertions(+) create mode 100644 extensions/SharkPool/Image-Effects diff --git a/extensions/SharkPool/Image-Effects b/extensions/SharkPool/Image-Effects new file mode 100644 index 0000000000..3edef9a970 --- /dev/null +++ b/extensions/SharkPool/Image-Effects @@ -0,0 +1,1285 @@ +// Name: Image Effects +// ID: imgEffectsSP +// Description: Apply a variety of new effects to the data URI of Images or Costumes. +// By: SharkPool + +// Version V.1.0.0 + +(function (Scratch) { + 'use strict'; + + if (!Scratch.extensions.unsandboxed) { + throw new Error('Image Effects extension must run unsandboxed'); + } + + const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=='; + + class imgEffectsSP { + constructor() { + this.cutoutX = 0; + this.cutoutY = 0; + } + + getInfo() { + return { + id: 'imgEffectsSP', + name: 'Image Effects', + menuIconURI, + color1: '#9966FF', + color2: '#774DCB', + blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: 'Effects', + }, + { + opcode: 'convertHexToRGB', + blockType: Scratch.BlockType.REPORTER, + text: 'convert [HEX] to [CHANNEL]', + arguments: { + HEX: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#FF0000', + }, + CHANNEL: { + type: Scratch.ArgumentType.STRING, + menu: 'CHANNELS', + defaultValue: 'R', + }, + }, + }, + { + opcode: 'applyHueEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'apply hue R [R] G [G] B [B] to URI [SVG]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + R: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 255, + }, + G: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + + '---', + + { + opcode: 'applyEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%', + arguments: { + EFFECT: { + type: Scratch.ArgumentType.STRING, + menu: 'EFFECTS', + defaultValue: 'Saturation', + }, + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + PERCENTAGE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + }, + }, + { + opcode: 'applyBulgeEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + STRENGTH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + CENTER_X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + CENTER_Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + }, + }, + { + opcode: 'applyWaveEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + AMPX: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + AMPY: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + FREQX: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + FREQY: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + }, + }, + { + opcode: 'applyLineGlitchEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + PERCENTAGE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + DIRECT: { + type: Scratch.ArgumentType.STRING, + menu: 'POSITIONS', + defaultValue: 'X', + }, + WIDTH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + }, + }, + { + opcode: 'applyAbberationEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + PERCENTAGE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 5, + }, + COLOR1: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#ff0000', + }, + COLOR2: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#00f7ff', + }, + DIRECT: { + type: Scratch.ArgumentType.STRING, + menu: 'POSITIONS', + defaultValue: 'X', + }, + }, + }, + + '---', + + { + opcode: 'removeTransparencyEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'remove pixels [REMOVE] [THRESHOLD]% transparency from URI [SVG]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + THRESHOLD: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 50, + }, + REMOVE: { + type: Scratch.ArgumentType.STRING, + menu: 'REMOVAL', + defaultValue: 'under', + }, + }, + }, + { + opcode: 'applyEdgeOutlineEffect', + blockType: Scratch.BlockType.REPORTER, + text: 'add outline to URI [SVG] with thickness [THICKNESS] and r [R] g [G] b [B] a [A]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + THICKNESS: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1, + }, + R: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 255, + }, + G: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + B: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + A: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 255, + }, + }, + }, + { + blockType: Scratch.BlockType.LABEL, + text: 'Clipping', + }, + { + opcode: 'clipImage', + blockType: Scratch.BlockType.REPORTER, + text: 'clip [CUTOUT] from [MAIN]', + arguments: { + MAIN: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'source-uri-here' + }, + CUTOUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'cutout-uri-here' + } + } + }, + { + opcode: 'setCutout', + blockType: Scratch.BlockType.COMMAND, + text: 'set clipping position to x [X] y [Y]', + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + } + } + }, + { + opcode: 'changeCutout', + blockType: Scratch.BlockType.COMMAND, + text: 'change clipping position by x [X] y [Y]', + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + } + } + }, + { + opcode: 'currentCut', + blockType: Scratch.BlockType.REPORTER, + text: 'clipping [POS]', + arguments: { + POS: { + type: Scratch.ArgumentType.STRING, + menu: 'POSITIONS', + defaultValue: 'X' + } + } + }, + { + blockType: Scratch.BlockType.LABEL, + text: 'Image Conversion', + }, + { + opcode: 'svgToBitmap', + blockType: Scratch.BlockType.REPORTER, + text: 'svg content [SVG] to bitmap width [WIDTH] height [HEIGHT]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '', + }, + WIDTH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + HEIGHT: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + ], + menus: { + CHANNELS: { + acceptReporters: true, + items: [ + 'R', + 'G', + 'B' + ] + }, + POSITIONS: ['X', 'Y'], + REMOVAL: ['under', 'over'], + EFFECTS: { + acceptReporters: true, + items: [ + 'Saturation', + 'Glitch', + 'Chunk Glitch', + 'Clip Glitch', + 'Vignette', + 'Ripple', + 'Displacement', + 'Posterize', + 'Blur', + 'Scanlines', + 'Grain', + 'Cubism' + ] + } + }, + }; + } + + convertHexToRGB(args) { + const hexColor = args.HEX; + const channel = args.CHANNEL; + + const r = parseInt(hexColor.substring(1, 3), 16); + const g = parseInt(hexColor.substring(3, 5), 16); + const b = parseInt(hexColor.substring(5, 7), 16); + + if (channel === 'R') { + return r; + } else if (channel === 'G') { + return g; + } else if (channel === 'B') { + return b; + } + } + + applyHueEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const r = args.R; + const g = args.G; + const b = args.B; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + await this.applyHue(imageData, r, g, b); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyHue(imageData, r, g, b) { + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + data[i] = Math.min(255, data[i] * r / 255); + data[i + 1] = Math.min(255, data[i + 1] * g / 255); + data[i + 2] = Math.min(255, data[i + 2] * b / 255); + } + } + + applyEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const percentage = (args.PERCENTAGE !== '') ? args.PERCENTAGE : 100; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const sEffect = args.EFFECT; + switch (sEffect) { + case 'Glitch': + this.applyGlitch(imageData, percentage); + break; + case 'Chunk Glitch': + this.applyChunkGlitch(imageData, percentage); + break; + case 'Clip Glitch': + this.applyClipGlitch(imageData, percentage); + break; + case 'Vignette': + this.applyVignette(imageData, percentage); + break; + case 'Displacement': + this.applyDisplacement(imageData, percentage); + break; + case 'Ripple': + this.applyRipple(imageData, percentage); + break; + case 'Posterize': + this.applyPosterize(imageData, percentage); + break; + case 'Blur': + this.applyBlur(imageData, percentage); + break; + case 'Scanlines': + this.applyScanlines(imageData, percentage); + break; + case 'Grain': + this.applyOldFilmGrain(imageData, percentage); + break; + case 'Cubism': + this.applyCubism(imageData, percentage); + break; + default: + this.applySaturation(imageData, percentage); + break; + } + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applySaturation(imageData, percentage) { + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const avg = (r + g + b) / 3; + data[i] = avg + (r - avg) * (percentage / 100); + data[i + 1] = avg + (g - avg) * (percentage / 100); + data[i + 2] = avg + (b - avg) * (percentage / 100); + } + } + + setCutout(args) { + this.cutoutX = Scratch.Cast.toNumber(args.X); + this.cutoutY = Scratch.Cast.toNumber(args.Y); + } + + changeCutout(args) { + this.cutoutX = this.cutoutX + Scratch.Cast.toNumber(args.X); + this.cutoutY = this.cutoutY + Scratch.Cast.toNumber(args.Y); + } + + currentCut(args) { + if (args.POS === 'X') { + return this.cutoutX; + } else { + return this.cutoutY; + } + } + + clipImage(args) { + return new Promise((resolve, reject) => { + const mainImage = new Image(); + mainImage.onload = () => { + const cutoutImage = new Image(); + cutoutImage.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = mainImage.width; + canvas.height = mainImage.height; + const context = canvas.getContext('2d'); + const cutX = this.cutoutX + (mainImage.width / 2) - (cutoutImage.width / 2); + const cutY = this.cutoutY - (mainImage.height / 2) + (cutoutImage.height / 2); + + context.drawImage(mainImage, 0, 0); + context.globalCompositeOperation = 'destination-in'; + context.drawImage(cutoutImage, cutX, cutY * -1); + context.globalCompositeOperation = 'source-over'; + + const clippedDataURI = canvas.toDataURL('image/png'); + resolve(clippedDataURI); + }; + cutoutImage.src = args.CUTOUT; + }; + mainImage.src = args.MAIN; + }); + } + + applyGlitch(imageData, percentage) { + const data = imageData.data; + const percent = Scratch.Cast.toNumber(percentage); + for (let i = 0; i < data.length; i += 4) { + const randomChance = Math.random() * 100; + + if (randomChance <= percentage) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + + const negative = Math.random() < 0.5 ? -1 : 1; + const randomOffsetR = Math.random() * ((percentage * 1.5) * negative); + const randomOffsetG = Math.random() * ((percentage * 1.5) * negative); + const randomOffsetB = Math.random() * ((percentage * 1.5) * negative); + + data[i] = (r + randomOffsetR) % 256; + data[i + 1] = (g + randomOffsetG) % 256; + data[i + 2] = (b + randomOffsetB) % 256; + } + } + } + + applyVignette(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + + const centerX = width / 2; + const centerY = height / 2; + + const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY); + const percent = Scratch.Cast.toNumber(percentage); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const distanceX = Math.abs(x - centerX); + const distanceY = Math.abs(y - centerY); + const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); + let vignetteAmount = ''; + if (percent < 0) { + vignetteAmount = 1 - (distance / maxDistance) * (percent / 100); + } else { + vignetteAmount = ((maxDistance - distance) / maxDistance) * (percent / 100); + } + + data[index] = Math.max(0, Math.min(255, data[index] * vignetteAmount)); + data[index + 1] = Math.max(0, Math.min(255, data[index + 1] * vignetteAmount)); + data[index + 2] = Math.max(0, Math.min(255, data[index + 2] * vignetteAmount)); + } + } + } + + applyRipple(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const centerX = width / 2; + const centerY = height / 2; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const dx = x - centerX; + const dy = y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + const offset = Math.sin(distance * (percentage / 100)) * (percentage / 100); + const sourceX = Math.floor(x + offset); + const sourceY = Math.floor(y); + + if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { + const sourceIndex = (sourceY * width + sourceX) * 4; + if (data[sourceIndex + 3] > 0) { + data[index] = data[sourceIndex]; + data[index + 1] = data[sourceIndex + 1]; + data[index + 2] = data[sourceIndex + 2]; + data[index + 3] = data[sourceIndex + 3]; + } else { + data[index + 3] = 0; + } + } else { + data[index + 3] = 0; + } + } + } + } + + applyDisplacement(imageData, displacementAmount) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const newData = new Uint8ClampedArray(data.length); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const srcX = x + Math.floor(Math.random() * displacementAmount * 2 - displacementAmount); + const srcY = y + Math.floor(Math.random() * displacementAmount * 2 - displacementAmount); + + if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { + const srcIndex = (srcY * width + srcX) * 4; + const dstIndex = (y * width + x) * 4; + + newData[dstIndex] = data[srcIndex]; + newData[dstIndex + 1] = data[srcIndex + 1]; + newData[dstIndex + 2] = data[srcIndex + 2]; + newData[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + data.set(newData); + } + + applyPosterize(imageData, percentage) { + const data = imageData.data; + const numLevels = Math.max(percentage / 10, 1); + + for (let i = 0; i < data.length; i += 4) { + data[i] = Math.round(data[i] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); + data[i + 1] = Math.round(data[i + 1] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); + data[i + 2] = Math.round(data[i + 2] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); + } + } + + applyBulgeEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + let centerX = (args.CENTER_X !== '') ? args.CENTER_X / 100 : 0; + let centerY = (args.CENTER_Y !== '') ? args.CENTER_Y / -100: 0; + const strength = (args.STRENGTH !== '') ? args.STRENGTH / 100 : 0; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + centerX = centerX + (img.width / 200); + centerY = centerY + (img.height / 200); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyBulge(imageData, centerX, centerY, strength); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyBulge(imageData, centerX, centerY, strength) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const newData = new Uint8ClampedArray(data.length); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const dx = (x / width - centerX) * 2; + const dy = (y / height - centerY) * 2; + const distance = Math.sqrt(dx * dx + dy * dy); + const bulge = Math.pow(distance, strength); + const srcX = Math.floor(x + (dx * bulge * width) - (dx * width)); + const srcY = Math.floor(y + (dy * bulge * height) - (dy * height)); + + if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { + const srcIndex = (srcY * width + srcX) * 4; + const dstIndex = (y * width + x) * 4; + newData[dstIndex] = data[srcIndex]; + newData[dstIndex + 1] = data[srcIndex + 1]; + newData[dstIndex + 2] = data[srcIndex + 2]; + newData[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + data.set(newData); + } + + applyWaveEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const amplitudeX = (args.AMPX !== '') ? args.AMPX / 10 : 0; + const amplitudeY = (args.AMPY !== '') ? args.AMPY / 10 : 0; + const frequencyX = (args.FREQX !== '') ? args.FREQX / 100 : 0; + const frequencyY = (args.FREQY !== '') ? args.FREQY / 100 : 0; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const newData = new Uint8ClampedArray(data.length); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const srcX = Math.floor(x + amplitudeX * Math.sin(y * frequencyX)); + const srcY = Math.floor(y + amplitudeY * Math.sin(x * frequencyY)); + + if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { + const srcIndex = (srcY * width + srcX) * 4; + const dstIndex = (y * width + x) * 4; + + newData[dstIndex] = data[srcIndex]; + newData[dstIndex + 1] = data[srcIndex + 1]; + newData[dstIndex + 2] = data[srcIndex + 2]; + newData[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + data.set(newData); + } + + applyBlur(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const radius = (percentage > 1) ? Math.floor((percentage / 100) * 10) : 0; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let r = 0, g = 0, b = 0, a = 0, count = 0; + + for (let ky = -radius; ky <= radius; ky++) { + for (let kx = -radius; kx <= radius; kx++) { + const offsetX = x + kx; + const offsetY = y + ky; + + if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { + const pixelIndex = (offsetY * width + offsetX) * 4; + + r += data[pixelIndex]; + g += data[pixelIndex + 1]; + b += data[pixelIndex + 2]; + a += data[pixelIndex + 3]; + count++; + } + } + } + + const pixelIndex = (y * width + x) * 4; + if (a === 0) { + data[pixelIndex + 3] = a / count; + } else { + data[pixelIndex] = r / count; + data[pixelIndex + 1] = g / count; + data[pixelIndex + 2] = b / count; + data[pixelIndex + 3] = a / count; + } + } + } + } + + applyLineGlitchEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const percentage = (args.PERCENTAGE !== '') ? args.PERCENTAGE / 100 : 0; + const direction = args.DIRECT; + const width = (args.WIDTH !== '') ? args.WIDTH / 50 : 0; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyLineGlitch(imageData, percentage, direction, width); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyLineGlitch(imageData, percentage, direction, width) { + const data = imageData.data; + const imgWidth = imageData.width; + const imgHeight = imageData.height; + const numLines = Math.floor(imgHeight * percentage); + + for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { + const linePosition = Math.floor(Math.random() * imgHeight); + const lineStart = linePosition - Math.floor(width / 2); + const lineEnd = lineStart + width; + + if (direction === 'Y') { + for (let y = 0; y < imgHeight; y++) { + for (let x = lineStart; x < lineEnd; x++) { + const srcX = x; + const srcY = linePosition; + + if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + const srcIndex = (srcY * imgWidth + srcX) * 4; + const dstIndex = (y * imgWidth + x) * 4; + + data[dstIndex] = data[srcIndex]; + data[dstIndex + 1] = data[srcIndex + 1]; + data[dstIndex + 2] = data[srcIndex + 2]; + data[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + } else { + for (let y = lineStart; y < lineEnd; y++) { + for (let x = 0; x < imgWidth; x++) { + const srcX = linePosition; + const srcY = y; + + if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + const srcIndex = (srcY * imgWidth + srcX) * 4; + const dstIndex = (y * imgWidth + x) * 4; + + data[dstIndex] = data[srcIndex]; + data[dstIndex + 1] = data[srcIndex + 1]; + data[dstIndex + 2] = data[srcIndex + 2]; + data[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + } + } + } + + applyChunkGlitch(imageData, percentage) { + const newWidth = percentage / 10; + const data = imageData.data; + const imgWidth = imageData.width; + const imgHeight = imageData.height; + const numLines = Math.floor(imgWidth * 1); + + for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { + const linePosition = Math.floor(Math.random() * imgHeight); + const lineStart = linePosition - Math.floor(newWidth / 2); + const lineEnd = lineStart + newWidth; + + for (let y = 0; y < imgHeight; y++) { + for (let x = lineStart; x < lineEnd; x++) { + const srcX = linePosition; + const srcY = y; + + if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + const srcIndex = (srcY * imgWidth + srcX) * 4; + const dstIndex = (y * imgWidth + x) * 4; + + data[dstIndex] = data[srcIndex]; + data[dstIndex + 1] = data[srcIndex + 1]; + data[dstIndex + 2] = data[srcIndex + 2]; + data[dstIndex + 3] = data[srcIndex + 3]; + } + } + } + } + } + + removeTransparencyEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const threshold = (args.THRESHOLD !== '') ? args.THRESHOLD / 100 : 0; + const removeUnder = args.REMOVE === 'under'; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyTransparencyRemoval(imageData, threshold, removeUnder); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyTransparencyRemoval(imageData, threshold, removeUnder) { + const data = imageData.data; + const pixelCount = data.length / 4; + + for (let i = 0; i < pixelCount; i++) { + const alpha = data[i * 4 + 3] / 255; + if ((removeUnder && alpha < threshold) || (!removeUnder && alpha > threshold)) { + data[i * 4 + 3] = 0; + } + } + } + + applyEdgeOutlineEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const thickness = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); + const r = args.R; + const g = args.G; + const b = args.B; + const a = args.A; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyOutline(imageData, thickness, r, g, b, a); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyOutline(imageData, thickness, r, g, b, a) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const outlineColor = [r, g, b, a]; + const copyData = new Uint8ClampedArray(data); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const alpha = data[index + 3]; + + if (alpha < 255) { + for (let dy = -thickness; dy <= thickness; dy++) { + for (let dx = -thickness; dx <= thickness; dx++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + const neighborIndex = (ny * width + nx) * 4; + const neighborAlpha = copyData[neighborIndex + 3]; + + if (neighborAlpha === 255) { + data[index] = outlineColor[0]; + data[index + 1] = outlineColor[1]; + data[index + 2] = outlineColor[2]; + data[index + 3] = outlineColor[3]; + break; + } + } + } + } + } + } + } + } + + applyClipGlitch(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const percent = percentage / 100; + + const numPixelsToEnlarge = Math.floor(percent / 100 * (width * height)); + const maxEnlargeFactor = 1.5 + (percent / 200); + + for (let i = 0; i < numPixelsToEnlarge; i++) { + const x = Math.floor(Math.random() * width); + const y = Math.floor(Math.random() * height); + const index = (y * width + x) * 4; + + const enlargeFactor = 1 + Math.random() * maxEnlargeFactor; + + const blurRadius = Math.floor(enlargeFactor * 4); + + for (let offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) { + for (let offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) { + const newX = x + offsetX; + const newY = y + offsetY; + + if (newX >= 0 && newX < width && newY >= 0 && newY < height) { + const newIndex = (newY * width + newX) * 4; + + data[newIndex] = data[index]; + data[newIndex + 1] = data[index + 1]; + data[newIndex + 2] = data[index + 2]; + data[newIndex + 3] = data[index + 3]; + } + } + } + } + } + + applyScanlines(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const scanlineHeight = Math.floor(height / 100); + const percent = percentage / 100; + + for (let y = 0; y < height; y++) { + if (Math.random() < percent) { + const scanlineBrightness = Math.random() * (percentage / 2); + + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + data[index] = Math.min(data[index] + scanlineBrightness, 255); + data[index + 1] = Math.min(data[index + 1] + scanlineBrightness, 255); + data[index + 2] = Math.min(data[index + 2] + scanlineBrightness, 255); + } + } + } + } + + applyOldFilmGrain(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + const rand = Math.random(); + + if (rand < percentage) { + const grain = Math.floor(Math.random() * percentage); + data[index] += grain; + data[index + 1] += grain; + data[index + 2] += grain; + } + } + } + } + + applyCubism(imageData, percentage) { + const data = imageData.data; + const width = imageData.width; + const height = imageData.height; + const percent = (percentage === 0 || percentage === '') ? 1 : Math.abs(Scratch.Cast.toNumber(percentage)); + + for (let y = 0; y < height; y += percent) { + for (let x = 0; x < width; x += percent) { + const startX = x; + const endX = Math.min(x + percent, width); + const startY = y; + const endY = Math.min(y + percent, height); + const avgColor = [0, 0, 0]; + + for (let j = startY; j < endY; j++) { + for (let i = startX; i < endX; i++) { + const index = (j * width + i) * 4; + avgColor[0] += data[index]; + avgColor[1] += data[index + 1]; + avgColor[2] += data[index + 2]; + } + } + + const totalPixels = (endX - startX) * (endY - startY); + avgColor[0] /= totalPixels; + avgColor[1] /= totalPixels; + avgColor[2] /= totalPixels; + + for (let j = startY; j < endY; j++) { + for (let i = startX; i < endX; i++) { + const index = (j * width + i) * 4; + data[index] = avgColor[0]; + data[index + 1] = avgColor[1]; + data[index + 2] = avgColor[2]; + } + } + } + } + } + + svgToBitmap(args) { + const svgContent = args.SVG; + const width = Math.abs(Scratch.Cast.toNumber(args.WIDTH)); + const height = Math.abs(Scratch.Cast.toNumber(args.HEIGHT)); + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + + if (args.WIDTH < 0) { + ctx.translate(width, 0); + ctx.scale(-1, 1); + } + if (args.HEIGHT < 0) { + ctx.translate(0, height); + ctx.scale(1, -1); + } + ctx.drawImage(img, 0, 0, width, height); + const imageData = ctx.getImageData(0, 0, width, height); + const newCanvas = document.createElement('canvas'); + newCanvas.width = width; + newCanvas.height = height; + const newCtx = newCanvas.getContext('2d'); + newCtx.putImageData(imageData, 0, 0); + const dataUri = newCanvas.toDataURL(); + resolve(dataUri); + }; + img.src = `data:image/svg+xml;base64,${btoa(svgContent)}`; + }); + } + + applyAbberationEffect(args) { + return new Promise((resolve) => { + const svgDataUri = args.SVG; + const percentage = args.PERCENTAGE; + const color1 = args.COLOR1; + const color2 = args.COLOR2; + const direction = args.DIRECT; + + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width + Math.abs(percentage) * 5; + canvas.height = img.height + Math.abs(percentage) * 5; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5); + + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyChromaticAberration(imageData, color1, color2, percentage, direction); + ctx.putImageData(imageData, 0, 0); + + const modifiedDataUrl = canvas.toDataURL(); + resolve(modifiedDataUrl); + }; + img.src = svgDataUri; + }); + } + + applyChromaticAberration(imageData, color1, color2, percentage, direction) { + const data = imageData.data; + let width = imageData.width; + let height = imageData.height; + const copy1 = new Uint8ClampedArray(data.length); + const copy2 = new Uint8ClampedArray(data.length); + + const hexToRGB = (hex) => [ + parseInt(hex.slice(1, 3), 16), + parseInt(hex.slice(3, 5), 16), + parseInt(hex.slice(5, 7), 16), + ]; + + const rgb1 = hexToRGB(color1); + const rgb2 = hexToRGB(color2); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const srcIndex = (y * width + x) * 4; + const r = data[srcIndex]; + const g = data[srcIndex + 1]; + const b = data[srcIndex + 2]; + const a = data[srcIndex + 3] / 1; + + let newX1, newY1, newX2, newY2; + + if (direction === 'X') { + newX1 = x + Math.floor((width / 2) * (percentage / 100)); + newY1 = y; + newX2 = x - Math.floor((width / 2) * (percentage / 100)); + newY2 = y; + } else { + newX1 = x; + newY1 = y + Math.floor((height / 2) * (percentage / 100)); + newX2 = x; + newY2 = y - Math.floor((height / 2) * (percentage / 100)); + } + + newX1 = Math.max(0, Math.min(width - 1, newX1)); + newY1 = Math.max(0, Math.min(height - 1, newY1)); + newX2 = Math.max(0, Math.min(width - 1, newX2)); + newY2 = Math.max(0, Math.min(height - 1, newY2)); + + const newR1 = data[(newY1 * width + newX1) * 4]; + const newG1 = data[(newY1 * width + newX1) * 4 + 1]; + const newB1 = data[(newY1 * width + newX1) * 4 + 2]; + + const newR2 = data[(newY2 * width + newX2) * 4]; + const newG2 = data[(newY2 * width + newX2) * 4 + 1]; + const newB2 = data[(newY2 * width + newX2) * 4 + 2]; + + const leftColor = [ + (rgb1[0] * r) / 255, + (rgb1[1] * g) / 255, + (rgb1[2] * b) / 255, + ]; + const rightColor = [ + (rgb2[0] * r) / 255, + (rgb2[1] * g) / 255, + (rgb2[2] * b) / 255, + ]; + + const leftIndex = (newY1 * width + newX1) * 4; + const rightIndex = (newY2 * width + newX2) * 4; + + copy1[leftIndex] = leftColor[0]; + copy1[leftIndex + 1] = leftColor[1]; + copy1[leftIndex + 2] = leftColor[2]; + copy1[leftIndex + 3] = a; + + copy2[rightIndex] = rightColor[0]; + copy2[rightIndex + 1] = rightColor[1]; + copy2[rightIndex + 2] = rightColor[2]; + copy2[rightIndex + 3] = a; + } + } + + for (let i = 0; i < data.length; i++) { + data[i] = Math.max(0, Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2)); + } + } + } + + Scratch.extensions.register(new imgEffectsSP()); +})(Scratch); From a5ba801dceb8361d165bc839ec2c850a9a8cf672 Mon Sep 17 00:00:00 2001 From: SharkPool-SP <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 20 Sep 2023 00:40:44 -0700 Subject: [PATCH 02/26] Update Image-Effects --- .../{Image-Effects => Image-Effects.js} | 835 +++++++++++++----- 1 file changed, 628 insertions(+), 207 deletions(-) rename extensions/SharkPool/{Image-Effects => Image-Effects.js} (52%) diff --git a/extensions/SharkPool/Image-Effects b/extensions/SharkPool/Image-Effects.js similarity index 52% rename from extensions/SharkPool/Image-Effects rename to extensions/SharkPool/Image-Effects.js index 3edef9a970..ad63656a2e 100644 --- a/extensions/SharkPool/Image-Effects +++ b/extensions/SharkPool/Image-Effects.js @@ -3,59 +3,62 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.1.0.0 +// Version V.1.3.0 (function (Scratch) { - 'use strict'; + "use strict"; if (!Scratch.extensions.unsandboxed) { - throw new Error('Image Effects extension must run unsandboxed'); + throw new Error("Image Effects extension must run unsandboxed"); } - const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=='; + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; class imgEffectsSP { constructor() { this.cutoutX = 0; this.cutoutY = 0; + this.scale = 100; + this.cutoutDirection = 90; } getInfo() { return { - id: 'imgEffectsSP', - name: 'Image Effects', + id: "imgEffectsSP", + name: "Image Effects", menuIconURI, - color1: '#9966FF', - color2: '#774DCB', + color1: "#9966FF", + color2: "#774DCB", blocks: [ { blockType: Scratch.BlockType.LABEL, - text: 'Effects', + text: "Effects", }, { - opcode: 'convertHexToRGB', + opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, - text: 'convert [HEX] to [CHANNEL]', + text: "convert [HEX] to [CHANNEL]", arguments: { HEX: { type: Scratch.ArgumentType.COLOR, - defaultValue: '#FF0000', - }, + defaultValue: "#FF0000", + }, CHANNEL: { type: Scratch.ArgumentType.STRING, - menu: 'CHANNELS', - defaultValue: 'R', + menu: "CHANNELS", + defaultValue: "R", }, }, }, { - opcode: 'applyHueEffect', + opcode: "applyHueEffect", blockType: Scratch.BlockType.REPORTER, - text: 'apply hue R [R] G [G] B [B] to URI [SVG]', + text: "apply hue R [R] G [G] B [B] to URI [SVG]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, R: { type: Scratch.ArgumentType.NUMBER, @@ -72,21 +75,21 @@ }, }, - '---', + "---", { - opcode: 'applyEffect', + opcode: "applyEffect", blockType: Scratch.BlockType.REPORTER, - text: 'set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%', + text: "set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%", arguments: { EFFECT: { type: Scratch.ArgumentType.STRING, - menu: 'EFFECTS', - defaultValue: 'Saturation', + menu: "EFFECTS", + defaultValue: "Saturation", }, SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, @@ -95,13 +98,13 @@ }, }, { - opcode: 'applyBulgeEffect', + opcode: "applyBulgeEffect", blockType: Scratch.BlockType.REPORTER, - text: 'set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]', + text: "set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, STRENGTH: { type: Scratch.ArgumentType.NUMBER, @@ -118,13 +121,13 @@ }, }, { - opcode: 'applyWaveEffect', + opcode: "applyWaveEffect", blockType: Scratch.BlockType.REPORTER, - text: 'set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]', + text: "set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, AMPX: { type: Scratch.ArgumentType.NUMBER, @@ -145,13 +148,13 @@ }, }, { - opcode: 'applyLineGlitchEffect', + opcode: "applyLineGlitchEffect", blockType: Scratch.BlockType.REPORTER, - text: 'set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]', + text: "set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, @@ -159,8 +162,8 @@ }, DIRECT: { type: Scratch.ArgumentType.STRING, - menu: 'POSITIONS', - defaultValue: 'X', + menu: "POSITIONS", + defaultValue: "X", }, WIDTH: { type: Scratch.ArgumentType.NUMBER, @@ -169,13 +172,13 @@ }, }, { - opcode: 'applyAbberationEffect', + opcode: "applyAbberationEffect", blockType: Scratch.BlockType.REPORTER, - text: 'set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis', + text: "set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, @@ -183,30 +186,30 @@ }, COLOR1: { type: Scratch.ArgumentType.COLOR, - defaultValue: '#ff0000', + defaultValue: "#ff0000", }, COLOR2: { type: Scratch.ArgumentType.COLOR, - defaultValue: '#00f7ff', + defaultValue: "#00f7ff", }, DIRECT: { type: Scratch.ArgumentType.STRING, - menu: 'POSITIONS', - defaultValue: 'X', + menu: "POSITIONS", + defaultValue: "X", }, }, }, - '---', + "---", { - opcode: 'removeTransparencyEffect', + opcode: "removeTransparencyEffect", blockType: Scratch.BlockType.REPORTER, - text: 'remove pixels [REMOVE] [THRESHOLD]% transparency from URI [SVG]', + text: "remove pixels from URI [SVG] [REMOVE] [THRESHOLD]% transparency", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, THRESHOLD: { type: Scratch.ArgumentType.NUMBER, @@ -214,19 +217,19 @@ }, REMOVE: { type: Scratch.ArgumentType.STRING, - menu: 'REMOVAL', - defaultValue: 'under', + menu: "REMOVAL", + defaultValue: "under", }, }, }, { - opcode: 'applyEdgeOutlineEffect', + opcode: "applyEdgeOutlineEffect", blockType: Scratch.BlockType.REPORTER, - text: 'add outline to URI [SVG] with thickness [THICKNESS] and r [R] g [G] b [B] a [A]', + text: "add outline to URI [SVG] with thickness [THICKNESS] and r [R] g [G] b [B] a [A]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "data:,", }, THICKNESS: { type: Scratch.ArgumentType.NUMBER, @@ -252,27 +255,27 @@ }, { blockType: Scratch.BlockType.LABEL, - text: 'Clipping', + text: "Clipping", }, { - opcode: 'clipImage', + opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, - text: 'clip [CUTOUT] from [MAIN]', + text: "clip [CUTOUT] from [MAIN]", arguments: { MAIN: { type: Scratch.ArgumentType.STRING, - defaultValue: 'source-uri-here' + defaultValue: "source-uri-here", }, CUTOUT: { type: Scratch.ArgumentType.STRING, - defaultValue: 'cutout-uri-here' - } - } + defaultValue: "cutout-uri-here", + }, + }, }, { - opcode: 'setCutout', + opcode: "setCutout", blockType: Scratch.BlockType.COMMAND, - text: 'set clipping position to x [X] y [Y]', + text: "set clipping position to x [X] y [Y]", arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -281,13 +284,13 @@ Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0, - } - } + }, + }, }, { - opcode: 'changeCutout', + opcode: "changeCutout", blockType: Scratch.BlockType.COMMAND, - text: 'change clipping position by x [X] y [Y]', + text: "change clipping position by x [X] y [Y]", arguments: { X: { type: Scratch.ArgumentType.NUMBER, @@ -296,33 +299,87 @@ Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0, - } - } + }, + }, }, { - opcode: 'currentCut', + opcode: "currentCut", blockType: Scratch.BlockType.REPORTER, - text: 'clipping [POS]', + text: "clipping [POS]", arguments: { POS: { type: Scratch.ArgumentType.STRING, - menu: 'POSITIONS', - defaultValue: 'X' - } - } + menu: "POSITIONS", + defaultValue: "X", + }, + }, + }, + { + opcode: "setScale", + blockType: Scratch.BlockType.COMMAND, + text: "set clipping size to [SIZE]%", + arguments: { + SIZE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + { + opcode: "changeScale", + blockType: Scratch.BlockType.COMMAND, + text: "change clipping size by [SIZE]", + arguments: { + SIZE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 10, + }, + }, + }, + { + opcode: "currentScale", + blockType: Scratch.BlockType.REPORTER, + text: "clipping size", + }, + { + opcode: "setDirection", + blockType: Scratch.BlockType.COMMAND, + text: "set clipping direction to [ANGLE]", + arguments: { + ANGLE: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 90, + }, + }, + }, + { + opcode: "changeDirection", + blockType: Scratch.BlockType.COMMAND, + text: "change clipping direction by [ANGLE]", + arguments: { + ANGLE: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: 15, + }, + }, + }, + { + opcode: "currentDir", + blockType: Scratch.BlockType.REPORTER, + text: "clipping direction", }, { blockType: Scratch.BlockType.LABEL, - text: 'Image Conversion', + text: "Image Conversions", }, { - opcode: 'svgToBitmap', + opcode: "svgToBitmap", blockType: Scratch.BlockType.REPORTER, - text: 'svg content [SVG] to bitmap width [WIDTH] height [HEIGHT]', + text: "convert svg content [SVG] to bitmap with width [WIDTH] height [HEIGHT]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, - defaultValue: '', + defaultValue: "", }, WIDTH: { type: Scratch.ArgumentType.NUMBER, @@ -334,54 +391,129 @@ }, }, }, + { + opcode: "convertImageToSVG", + blockType: Scratch.BlockType.REPORTER, + text: "convert bitmap URI [URI] to svg [TYPE]", + arguments: { + URI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data:,", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "fileType", + defaultValue: "content", + }, + }, + }, + { + opcode: "makeSVGimage", + blockType: Scratch.BlockType.REPORTER, + text: "make svg with image URI [URI] to svg [TYPE]", + arguments: { + URI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data:,", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "fileType", + defaultValue: "content", + }, + }, + }, + { + opcode: "audioToImage", + blockType: Scratch.BlockType.REPORTER, + text: "convert audio URI [AUDIO_URI] to PNG with width [W] and height [H]", + arguments: { + AUDIO_URI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "audio_uri_here", + }, + W: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + H: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 100, + }, + }, + }, + + "---", + + { + opcode: "skewSVG", + blockType: Scratch.BlockType.REPORTER, + text: "skew SVG content [SVG] at x [Y] y [X] as [TYPE]", + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 0, + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "fileType", + defaultValue: "content", + }, + }, + }, ], menus: { CHANNELS: { acceptReporters: true, - items: [ - 'R', - 'G', - 'B' - ] + items: ["R", "G", "B"], }, - POSITIONS: ['X', 'Y'], - REMOVAL: ['under', 'over'], + POSITIONS: ["X", "Y"], + REMOVAL: ["under", "over", "equal to"], + fileType: ["content", "dataURI"], EFFECTS: { acceptReporters: true, items: [ - 'Saturation', - 'Glitch', - 'Chunk Glitch', - 'Clip Glitch', - 'Vignette', - 'Ripple', - 'Displacement', - 'Posterize', - 'Blur', - 'Scanlines', - 'Grain', - 'Cubism' - ] - } + "Saturation", + "Glitch", + "Chunk Glitch", + "Clip Glitch", + "Vignette", + "Ripple", + "Displacement", + "Posterize", + "Blur", + "Scanlines", + "Grain", + "Cubism", + ], + }, }, }; } convertHexToRGB(args) { - const hexColor = args.HEX; - const channel = args.CHANNEL; - - const r = parseInt(hexColor.substring(1, 3), 16); - const g = parseInt(hexColor.substring(3, 5), 16); - const b = parseInt(hexColor.substring(5, 7), 16); - - if (channel === 'R') { - return r; - } else if (channel === 'G') { - return g; - } else if (channel === 'B') { - return b; - } + const hexColor = args.HEX; + const channel = args.CHANNEL; + + const r = parseInt(hexColor.substring(1, 3), 16); + const g = parseInt(hexColor.substring(3, 5), 16); + const b = parseInt(hexColor.substring(5, 7), 16); + + if (channel === "R") { + return r; + } else if (channel === "G") { + return g; + } else if (channel === "B") { + return b; + } } applyHueEffect(args) { @@ -393,10 +525,10 @@ const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -413,59 +545,59 @@ applyHue(imageData, r, g, b) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { - data[i] = Math.min(255, data[i] * r / 255); - data[i + 1] = Math.min(255, data[i + 1] * g / 255); - data[i + 2] = Math.min(255, data[i + 2] * b / 255); + data[i] = Math.min(255, (data[i] * r) / 255); + data[i + 1] = Math.min(255, (data[i + 1] * g) / 255); + data[i + 2] = Math.min(255, (data[i + 2] * b) / 255); } } applyEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const percentage = (args.PERCENTAGE !== '') ? args.PERCENTAGE : 100; + const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE : 100; const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const sEffect = args.EFFECT; + const sEffect = args.EFFECT; switch (sEffect) { - case 'Glitch': + case "Glitch": this.applyGlitch(imageData, percentage); break; - case 'Chunk Glitch': + case "Chunk Glitch": this.applyChunkGlitch(imageData, percentage); break; - case 'Clip Glitch': + case "Clip Glitch": this.applyClipGlitch(imageData, percentage); break; - case 'Vignette': + case "Vignette": this.applyVignette(imageData, percentage); break; - case 'Displacement': + case "Displacement": this.applyDisplacement(imageData, percentage); break; - case 'Ripple': + case "Ripple": this.applyRipple(imageData, percentage); break; - case 'Posterize': + case "Posterize": this.applyPosterize(imageData, percentage); break; - case 'Blur': + case "Blur": this.applyBlur(imageData, percentage); break; - case 'Scanlines': + case "Scanlines": this.applyScanlines(imageData, percentage); break; - case 'Grain': + case "Grain": this.applyOldFilmGrain(imageData, percentage); break; - case 'Cubism': + case "Cubism": this.applyCubism(imageData, percentage); break; default: @@ -504,33 +636,80 @@ this.cutoutY = this.cutoutY + Scratch.Cast.toNumber(args.Y); } + setScale(args) { + this.scale = Scratch.Cast.toNumber(args.SIZE); + } + + changeScale(args) { + this.scale = this.scale + Scratch.Cast.toNumber(args.SIZE); + } + + setDirection(args) { + this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE); + } + + changeDirection(args) { + let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); + if (direction > 180) { + direction = -180 + Scratch.Cast.toNumber(args.ANGLE); + } + if (direction < -180) { + direction = 180 + Scratch.Cast.toNumber(args.ANGLE); + } + this.cutoutDirection = direction; + } + currentCut(args) { - if (args.POS === 'X') { + if (args.POS === "X") { return this.cutoutX; } else { return this.cutoutY; } } + currentScale() { + return this.scale; + } + + currentDir() { + return this.cutoutDirection; + } + clipImage(args) { return new Promise((resolve, reject) => { const mainImage = new Image(); mainImage.onload = () => { const cutoutImage = new Image(); cutoutImage.onload = () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = mainImage.width; canvas.height = mainImage.height; - const context = canvas.getContext('2d'); - const cutX = this.cutoutX + (mainImage.width / 2) - (cutoutImage.width / 2); - const cutY = this.cutoutY - (mainImage.height / 2) + (cutoutImage.height / 2); + const context = canvas.getContext("2d"); + const scaledWidth = cutoutImage.width + this.scale; + const scaledHeight = cutoutImage.height + this.scale; + const cutX = this.cutoutX + mainImage.width / 2 - scaledWidth / 2; + const cutY = this.cutoutY - mainImage.height / 2 + scaledHeight / 2; context.drawImage(mainImage, 0, 0); - context.globalCompositeOperation = 'destination-in'; - context.drawImage(cutoutImage, cutX, cutY * -1); - context.globalCompositeOperation = 'source-over'; - - const clippedDataURI = canvas.toDataURL('image/png'); + context.globalCompositeOperation = "destination-in"; + const rotationAngle = + ((this.cutoutDirection + 270) * Math.PI) / 180; + context.translate( + cutX + scaledWidth / 2, + cutY * -1 + scaledHeight / 2 + ); + context.rotate(rotationAngle); + context.drawImage( + cutoutImage, + -scaledWidth / 2, + -scaledHeight / 2, + scaledWidth, + scaledHeight + ); + context.setTransform(1, 0, 0, 1, 0, 0); + context.globalCompositeOperation = "source-over"; + + const clippedDataURI = canvas.toDataURL("image/png"); resolve(clippedDataURI); }; cutoutImage.src = args.CUTOUT; @@ -551,9 +730,9 @@ const b = data[i + 2]; const negative = Math.random() < 0.5 ? -1 : 1; - const randomOffsetR = Math.random() * ((percentage * 1.5) * negative); - const randomOffsetG = Math.random() * ((percentage * 1.5) * negative); - const randomOffsetB = Math.random() * ((percentage * 1.5) * negative); + const randomOffsetR = Math.random() * (percentage * 1.5 * negative); + const randomOffsetG = Math.random() * (percentage * 1.5 * negative); + const randomOffsetB = Math.random() * (percentage * 1.5 * negative); data[i] = (r + randomOffsetR) % 256; data[i + 1] = (g + randomOffsetG) % 256; @@ -578,17 +757,29 @@ const index = (y * width + x) * 4; const distanceX = Math.abs(x - centerX); const distanceY = Math.abs(y - centerY); - const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); - let vignetteAmount = ''; + const distance = Math.sqrt( + distanceX * distanceX + distanceY * distanceY + ); + let vignetteAmount = ""; if (percent < 0) { - vignetteAmount = 1 - (distance / maxDistance) * (percent / 100); + vignetteAmount = 1 - (distance / maxDistance) * (percent / 100); } else { - vignetteAmount = ((maxDistance - distance) / maxDistance) * (percent / 100); + vignetteAmount = + ((maxDistance - distance) / maxDistance) * (percent / 100); } - data[index] = Math.max(0, Math.min(255, data[index] * vignetteAmount)); - data[index + 1] = Math.max(0, Math.min(255, data[index + 1] * vignetteAmount)); - data[index + 2] = Math.max(0, Math.min(255, data[index + 2] * vignetteAmount)); + data[index] = Math.max( + 0, + Math.min(255, data[index] * vignetteAmount) + ); + data[index + 1] = Math.max( + 0, + Math.min(255, data[index + 1] * vignetteAmount) + ); + data[index + 2] = Math.max( + 0, + Math.min(255, data[index + 2] * vignetteAmount) + ); } } } @@ -607,11 +798,17 @@ const dy = y - centerY; const distance = Math.sqrt(dx * dx + dy * dy); - const offset = Math.sin(distance * (percentage / 100)) * (percentage / 100); + const offset = + Math.sin(distance * (percentage / 100)) * (percentage / 100); const sourceX = Math.floor(x + offset); const sourceY = Math.floor(y); - if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { + if ( + sourceX >= 0 && + sourceX < width && + sourceY >= 0 && + sourceY < height + ) { const sourceIndex = (sourceY * width + sourceX) * 4; if (data[sourceIndex + 3] > 0) { data[index] = data[sourceIndex]; @@ -636,8 +833,16 @@ for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const srcX = x + Math.floor(Math.random() * displacementAmount * 2 - displacementAmount); - const srcY = y + Math.floor(Math.random() * displacementAmount * 2 - displacementAmount); + const srcX = + x + + Math.floor( + Math.random() * displacementAmount * 2 - displacementAmount + ); + const srcY = + y + + Math.floor( + Math.random() * displacementAmount * 2 - displacementAmount + ); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; @@ -658,27 +863,33 @@ const numLevels = Math.max(percentage / 10, 1); for (let i = 0; i < data.length; i += 4) { - data[i] = Math.round(data[i] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); - data[i + 1] = Math.round(data[i + 1] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); - data[i + 2] = Math.round(data[i + 2] * (numLevels - 1) / 255) * (255 / (numLevels - 1)); + data[i] = + Math.round((data[i] * (numLevels - 1)) / 255) * + (255 / (numLevels - 1)); + data[i + 1] = + Math.round((data[i + 1] * (numLevels - 1)) / 255) * + (255 / (numLevels - 1)); + data[i + 2] = + Math.round((data[i + 2] * (numLevels - 1)) / 255) * + (255 / (numLevels - 1)); } } applyBulgeEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - let centerX = (args.CENTER_X !== '') ? args.CENTER_X / 100 : 0; - let centerY = (args.CENTER_Y !== '') ? args.CENTER_Y / -100: 0; - const strength = (args.STRENGTH !== '') ? args.STRENGTH / 100 : 0; + let centerX = args.CENTER_X !== "" ? args.CENTER_X / 100 : 0; + let centerY = args.CENTER_Y !== "" ? args.CENTER_Y / -100 : 0; + const strength = args.STRENGTH !== "" ? args.STRENGTH / 100 : 0; const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - centerX = centerX + (img.width / 200); - centerY = centerY + (img.height / 200); - const ctx = canvas.getContext('2d'); + centerX = centerX + img.width / 200; + centerY = centerY + img.height / 200; + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -704,8 +915,8 @@ const dy = (y / height - centerY) * 2; const distance = Math.sqrt(dx * dx + dy * dy); const bulge = Math.pow(distance, strength); - const srcX = Math.floor(x + (dx * bulge * width) - (dx * width)); - const srcY = Math.floor(y + (dy * bulge * height) - (dy * height)); + const srcX = Math.floor(x + dx * bulge * width - dx * width); + const srcY = Math.floor(y + dy * bulge * height - dy * height); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; @@ -723,21 +934,27 @@ applyWaveEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const amplitudeX = (args.AMPX !== '') ? args.AMPX / 10 : 0; - const amplitudeY = (args.AMPY !== '') ? args.AMPY / 10 : 0; - const frequencyX = (args.FREQX !== '') ? args.FREQX / 100 : 0; - const frequencyY = (args.FREQY !== '') ? args.FREQY / 100 : 0; + const amplitudeX = args.AMPX !== "" ? args.AMPX / 10 : 0; + const amplitudeY = args.AMPY !== "" ? args.AMPY / 10 : 0; + const frequencyX = args.FREQX !== "" ? args.FREQX / 100 : 0; + const frequencyY = args.FREQY !== "" ? args.FREQY / 100 : 0; const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY); + this.applyWave( + imageData, + amplitudeX, + amplitudeY, + frequencyX, + frequencyY + ); ctx.putImageData(imageData, 0, 0); const modifiedDataUrl = canvas.toDataURL(); @@ -776,18 +993,27 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const radius = (percentage > 1) ? Math.floor((percentage / 100) * 10) : 0; + const radius = percentage > 1 ? Math.floor((percentage / 100) * 10) : 0; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - let r = 0, g = 0, b = 0, a = 0, count = 0; + let r = 0, + g = 0, + b = 0, + a = 0, + count = 0; for (let ky = -radius; ky <= radius; ky++) { for (let kx = -radius; kx <= radius; kx++) { const offsetX = x + kx; const offsetY = y + ky; - if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { + if ( + offsetX >= 0 && + offsetX < width && + offsetY >= 0 && + offsetY < height + ) { const pixelIndex = (offsetY * width + offsetX) * 4; r += data[pixelIndex]; @@ -815,16 +1041,16 @@ applyLineGlitchEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const percentage = (args.PERCENTAGE !== '') ? args.PERCENTAGE / 100 : 0; + const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE / 100 : 0; const direction = args.DIRECT; - const width = (args.WIDTH !== '') ? args.WIDTH / 50 : 0; + const width = args.WIDTH !== "" ? args.WIDTH / 50 : 0; const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -849,13 +1075,18 @@ const lineStart = linePosition - Math.floor(width / 2); const lineEnd = lineStart + width; - if (direction === 'Y') { + if (direction === "Y") { for (let y = 0; y < imgHeight; y++) { for (let x = lineStart; x < lineEnd; x++) { const srcX = x; const srcY = linePosition; - if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + if ( + srcX >= 0 && + srcX < imgWidth && + srcY >= 0 && + srcY < imgHeight + ) { const srcIndex = (srcY * imgWidth + srcX) * 4; const dstIndex = (y * imgWidth + x) * 4; @@ -872,7 +1103,12 @@ const srcX = linePosition; const srcY = y; - if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + if ( + srcX >= 0 && + srcX < imgWidth && + srcY >= 0 && + srcY < imgHeight + ) { const srcIndex = (srcY * imgWidth + srcX) * 4; const dstIndex = (y * imgWidth + x) * 4; @@ -921,15 +1157,15 @@ removeTransparencyEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const threshold = (args.THRESHOLD !== '') ? args.THRESHOLD / 100 : 0; - const removeUnder = args.REMOVE === 'under'; + const threshold = args.THRESHOLD !== "" ? args.THRESHOLD / 100 : 0; + const removeUnder = args.REMOVE; const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -949,7 +1185,13 @@ for (let i = 0; i < pixelCount; i++) { const alpha = data[i * 4 + 3] / 255; - if ((removeUnder && alpha < threshold) || (!removeUnder && alpha > threshold)) { + if ( + (removeUnder === "under" && alpha < threshold) || + (removeUnder === "over" && alpha > threshold) || + (removeUnder === "equal to" && + alpha > threshold - 0.01 && + alpha < threshold + 0.01) + ) { data[i * 4 + 3] = 0; } } @@ -966,10 +1208,10 @@ const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -1026,8 +1268,8 @@ const height = imageData.height; const percent = percentage / 100; - const numPixelsToEnlarge = Math.floor(percent / 100 * (width * height)); - const maxEnlargeFactor = 1.5 + (percent / 200); + const numPixelsToEnlarge = Math.floor((percent / 100) * (width * height)); + const maxEnlargeFactor = 1.5 + percent / 200; for (let i = 0; i < numPixelsToEnlarge; i++) { const x = Math.floor(Math.random() * width); @@ -1055,7 +1297,7 @@ } } } - + applyScanlines(imageData, percentage) { const data = imageData.data; const width = imageData.width; @@ -1070,8 +1312,14 @@ for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; data[index] = Math.min(data[index] + scanlineBrightness, 255); - data[index + 1] = Math.min(data[index + 1] + scanlineBrightness, 255); - data[index + 2] = Math.min(data[index + 2] + scanlineBrightness, 255); + data[index + 1] = Math.min( + data[index + 1] + scanlineBrightness, + 255 + ); + data[index + 2] = Math.min( + data[index + 2] + scanlineBrightness, + 255 + ); } } } @@ -1101,7 +1349,10 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const percent = (percentage === 0 || percentage === '') ? 1 : Math.abs(Scratch.Cast.toNumber(percentage)); + const percent = + percentage === 0 || percentage === "" + ? 1 + : Math.abs(Scratch.Cast.toNumber(percentage)); for (let y = 0; y < height; y += percent) { for (let x = 0; x < width; x += percent) { @@ -1144,10 +1395,10 @@ return new Promise((resolve) => { const img = new Image(); img.onload = () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (args.WIDTH < 0) { ctx.translate(width, 0); @@ -1159,10 +1410,10 @@ } ctx.drawImage(img, 0, 0, width, height); const imageData = ctx.getImageData(0, 0, width, height); - const newCanvas = document.createElement('canvas'); + const newCanvas = document.createElement("canvas"); newCanvas.width = width; newCanvas.height = height; - const newCtx = newCanvas.getContext('2d'); + const newCtx = newCanvas.getContext("2d"); newCtx.putImageData(imageData, 0, 0); const dataUri = newCanvas.toDataURL(); resolve(dataUri); @@ -1181,14 +1432,24 @@ const img = new Image(); img.onload = async () => { - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = img.width + Math.abs(percentage) * 5; canvas.height = img.height + Math.abs(percentage) * 5; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5); + const ctx = canvas.getContext("2d"); + ctx.drawImage( + img, + Math.abs(percentage) * 2.5, + Math.abs(percentage) * 2.5 + ); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyChromaticAberration(imageData, color1, color2, percentage, direction); + this.applyChromaticAberration( + imageData, + color1, + color2, + percentage, + direction + ); ctx.putImageData(imageData, 0, 0); const modifiedDataUrl = canvas.toDataURL(); @@ -1224,7 +1485,7 @@ let newX1, newY1, newX2, newY2; - if (direction === 'X') { + if (direction === "X") { newX1 = x + Math.floor((width / 2) * (percentage / 100)); newY1 = y; newX2 = x - Math.floor((width / 2) * (percentage / 100)); @@ -1276,8 +1537,168 @@ } for (let i = 0; i < data.length; i++) { - data[i] = Math.max(0, Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2)); + data[i] = Math.max( + 0, + Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2) + ); + } + } + + convertImageToSVG(args) { + return new Promise((resolve) => { + const img = new Image(); + img.src = args.URI; + + 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, img.width, img.height); + + const svg = document.createElementNS( + "http://www.w3.org/2000/svg", + "svg" + ); + svg.setAttribute("version", "1.1"); + svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + svg.setAttribute("width", img.width.toFixed(5)); + svg.setAttribute("height", img.height.toFixed(5)); + svg.setAttribute( + "viewBox", + `0,0,${img.width.toFixed(5)},${img.height.toFixed(5)}` + ); + const mergedColors = new Map(); + + for (let y = 0; y < img.height; y++) { + for (let x = 0; x < img.width; x++) { + const colorData = ctx.getImageData(x, y, 1, 1).data; + const alpha = colorData[3]; + + if (alpha === 0) { + continue; + } + + const color = `rgb(${colorData[0]}, ${colorData[1]}, ${colorData[2]})`; + const rightColorData = ctx.getImageData(x + 1, y, 1, 1).data; + const rightColor = `rgb(${rightColorData[0]}, ${rightColorData[1]}, ${rightColorData[2]})`; + + if (color === rightColor) { + const mergedPixel = mergedColors.get(color) || { + x1: x, + y1: y, + x2: x + 1, + y2: y, + }; + mergedPixel.x2++; + mergedColors.set(color, mergedPixel); + } else { + mergedColors.forEach((mergedPixel, colorKey) => { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + rect.setAttribute("x", mergedPixel.x1.toFixed(5)); + rect.setAttribute("y", mergedPixel.y1.toFixed(5)); + rect.setAttribute( + "width", + (mergedPixel.x2 - mergedPixel.x1 + 1).toFixed(5) + ); + rect.setAttribute( + "height", + (mergedPixel.y2 - mergedPixel.y1 + 1).toFixed(5) + ); + rect.setAttribute("fill", colorKey); + + svg.appendChild(rect); + }); + mergedColors.clear(); + } + } + } + let svgString = new XMLSerializer().serializeToString(svg); + if (args.TYPE === "dataURI") { + svgString = `data:image/svg+xml;base64,${btoa(svgString)}`; + } + resolve(svgString); + }; + }); + } + + makeSVGimage(args) { + const img = new Image(); + img.src = args.URI; + const width = img.width; + const height = img.height; + let base = ``; + if (args.TYPE === "dataURI") { + base = `data:image/svg+xml;base64,${btoa(base)}`; + } + return base; + } + + audioToImage(args) { + const audioURI = args.AUDIO_URI; + const imageWidth = Math.abs(Scratch.Cast.toString(args.W)); + const imageHeight = Math.abs(Scratch.Cast.toString(args.H)); + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = imageWidth; + canvas.height = imageHeight; + + for (let i = 0; i < audioURI.length; i++) { + const charCode = audioURI.charCodeAt(i); + const red = (charCode * 2) % 256; + const green = (charCode * 3) % 256; + const blue = (charCode * 4) % 256; + ctx.fillStyle = `rgb(${red},${green},${blue})`; + ctx.fillRect(i % imageWidth, Math.floor(i / imageWidth), 1, 1); + } + const dataURI = canvas.toDataURL("image/png"); + return dataURI; + } + + skewSVG(args) { + let SVG = args.SVG; + const parser = new DOMParser(); + const doc = parser.parseFromString(SVG, "image/svg+xml"); + const svgElement = doc.documentElement; + const originalWidth = parseFloat(svgElement.getAttribute("width")); + const originalHeight = parseFloat(svgElement.getAttribute("height")); + const newTransform = `matrix(1, ${args.X / 100}, ${ + args.Y / 100 + }, 1, ${Math.abs(args.X)}, ${Math.abs(args.Y)})`; + + svgElement.setAttribute("transform", newTransform); + + const skewX = Math.abs((args.X / 100) * originalWidth); + const skewY = Math.abs((args.Y / 100) * originalHeight); + const newViewBoxWidth = originalWidth + 2 * skewX; + const newViewBoxHeight = originalHeight + 2 * skewY; + let newViewBoxX = (newViewBoxWidth - originalWidth) / 2; + newViewBoxX = newViewBoxX < 0 || args.Y < 0 ? 0 : newViewBoxX; + let newViewBoxY = (newViewBoxHeight - originalHeight) / 2; + newViewBoxY = newViewBoxY < 0 || args.X < 0 ? 0 : newViewBoxY; + let offsetX = args.X < 0 ? 1 : 0; + let offsetY = args.Y < 0 ? 1 : 0; + const newViewBox = `${newViewBoxX} ${newViewBoxY} ${ + newViewBoxWidth - Math.abs(args.X * offsetX) + } ${newViewBoxHeight - Math.abs(args.Y * offsetY)}`; + svgElement.setAttribute("viewBox", newViewBox); + + const serializer = new XMLSerializer(); + SVG = serializer.serializeToString(svgElement); + if (args.TYPE === "dataURI") { + SVG = `data:image/svg+xml;base64,${btoa(SVG)}`; } + return SVG; } } From 1994d605d9310f4fbd33a76259e8d47b7d114c09 Mon Sep 17 00:00:00 2001 From: SharkPool-SP <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:11:41 -0700 Subject: [PATCH 03/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 192 +++++++++++++++++++++----- 1 file changed, 161 insertions(+), 31 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index ad63656a2e..013eaf9d3b 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.1.3.0 +// Version V.1.4.0 (function (Scratch) { "use strict"; @@ -15,12 +15,20 @@ const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; + function hexToRgb(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return [r, g, b]; + } + class imgEffectsSP { constructor() { this.cutoutX = 0; this.cutoutY = 0; this.scale = 100; this.cutoutDirection = 90; + this.softness = 10; } getInfo() { @@ -54,25 +62,65 @@ { opcode: "applyHueEffect", blockType: Scratch.BlockType.REPORTER, - text: "apply hue R [R] G [G] B [B] to URI [SVG]", + text: "apply hue [COLOR] to URI [SVG]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:,", }, - R: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 255, + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#FF0000", }, - G: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, + }, + }, + + "---", + + { + opcode: "deleteColor", + blockType: Scratch.BlockType.REPORTER, + text: "remove color [COLOR] from [DATA_URI]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#FF0000", }, - B: { + DATA_URI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data:,", + } + } + }, + { + opcode: "replaceColor", + blockType: Scratch.BlockType.REPORTER, + text: "replace color [COLOR] with [REPLACE_COLOR] from [DATA_URI]", + arguments: { + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#FF0000", + }, + REPLACE_COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#00ff22", + }, + DATA_URI: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data:,", + } + } + }, + { + opcode: "setSoftness", + blockType: Scratch.BlockType.COMMAND, + text: "set softness of color detection to [AMT]%", + arguments: { + AMT: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, + defaultValue: 10, }, - }, + } }, "---", @@ -225,7 +273,7 @@ { opcode: "applyEdgeOutlineEffect", blockType: Scratch.BlockType.REPORTER, - text: "add outline to URI [SVG] with thickness [THICKNESS] and r [R] g [G] b [B] a [A]", + text: "add outline to URI [SVG] with thickness [THICKNESS] and color [COLOR] opacity [A]%", arguments: { SVG: { type: Scratch.ArgumentType.STRING, @@ -235,21 +283,13 @@ type: Scratch.ArgumentType.NUMBER, defaultValue: 1, }, - R: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 255, - }, - G: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - B: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: "#FF0000", }, A: { type: Scratch.ArgumentType.NUMBER, - defaultValue: 255, + defaultValue: 100, }, }, }, @@ -519,9 +559,10 @@ applyHueEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const r = args.R; - const g = args.G; - const b = args.B; + const color = hexToRgb(args.COLOR); + const r = color[0]; + const g = color[1]; + const b = color[2]; const img = new Image(); img.onload = async () => { @@ -1201,10 +1242,11 @@ return new Promise((resolve) => { const svgDataUri = args.SVG; const thickness = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); - const r = args.R; - const g = args.G; - const b = args.B; - const a = args.A; + const color = hexToRgb(args.COLOR); + const r = color[0]; + const g = color[1]; + const b = color[2]; + const a = Math.min(Math.max(args.A, 0), 100) * 2.55; const img = new Image(); img.onload = async () => { @@ -1700,6 +1742,94 @@ } return SVG; } + + deleteColor(args) { + const hexColorToBeRemoved = args.COLOR; + const colorToBeRemoved = hexToRgb(hexColorToBeRemoved); + const dataURI = args.DATA_URI; + const canvasElement = document.createElement("canvas"); + const context = canvasElement.getContext("2d"); + const imageElement = new Image(); + const softness = this.softness; + + return new Promise(resolve => { + imageElement.onload = () => { + canvasElement.width = imageElement.width; + canvasElement.height = imageElement.height; + context.drawImage(imageElement, 0, 0); + const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + if ( + r >= colorToBeRemoved[0] - softness && + r <= colorToBeRemoved[0] + softness && + g >= colorToBeRemoved[1] - softness && + g <= colorToBeRemoved[1] + softness && + b >= colorToBeRemoved[2] - softness && + b <= colorToBeRemoved[2] + softness + ) { + data[i + 3] = 0; + } + } + context.putImageData(imageData, 0, 0); + const newDataURI = canvasElement.toDataURL("image/png"); + resolve(newDataURI); + }; + imageElement.src = dataURI; + }); + } + + replaceColor(args) { + const hexColorToBeRemoved = args.COLOR; + const ColorReplaced = args.REPLACE_COLOR; + const colorToBeRemoved = hexToRgb(hexColorToBeRemoved); + const colorToBeReplaced = hexToRgb(ColorReplaced); + const dataURI = args.DATA_URI; + const canvasElement = document.createElement("canvas"); + const context = canvasElement.getContext("2d"); + const imageElement = new Image(); + const softness = this.softness; + + return new Promise(resolve => { + imageElement.onload = () => { + canvasElement.width = imageElement.width; + canvasElement.height = imageElement.height; + context.drawImage(imageElement, 0, 0); + const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + if ( + r >= colorToBeRemoved[0] - softness && + r <= colorToBeRemoved[0] + softness && + g >= colorToBeRemoved[1] - softness && + g <= colorToBeRemoved[1] + softness && + b >= colorToBeRemoved[2] - softness && + b <= colorToBeRemoved[2] + softness + ) { + data[i] = colorToBeReplaced[0]; + data[i + 1] = colorToBeReplaced[1]; + data[i + 2] = colorToBeReplaced[2]; + } + } + context.putImageData(imageData, 0, 0); + const newDataURI = canvasElement.toDataURL("image/png"); + resolve(newDataURI); + }; + imageElement.src = dataURI; + }); + } + + setSoftness(args) { + this.softness = Scratch.Cast.toNumber(args.AMT); + } } Scratch.extensions.register(new imgEffectsSP()); From 43a0fbfedab49794a84e0745cdd683ebfa2daa29 Mon Sep 17 00:00:00 2001 From: SharkPool-SP <139097378+SharkPool-SP@users.noreply.github.com> Date: Thu, 21 Sep 2023 23:38:56 -0700 Subject: [PATCH 04/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 46 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 013eaf9d3b..5a5c51cfb4 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -89,8 +89,8 @@ DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:,", - } - } + }, + }, }, { opcode: "replaceColor", @@ -108,8 +108,8 @@ DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:,", - } - } + }, + }, }, { opcode: "setSoftness", @@ -120,7 +120,7 @@ type: Scratch.ArgumentType.NUMBER, defaultValue: 10, }, - } + }, }, "---", @@ -565,7 +565,7 @@ const b = color[2]; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -573,7 +573,7 @@ ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - await this.applyHue(imageData, r, g, b); + this.applyHue(imageData, r, g, b); ctx.putImageData(imageData, 0, 0); const modifiedDataUrl = canvas.toDataURL(); @@ -598,7 +598,7 @@ const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE : 100; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -924,7 +924,7 @@ const strength = args.STRENGTH !== "" ? args.STRENGTH / 100 : 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -981,7 +981,7 @@ const frequencyY = args.FREQY !== "" ? args.FREQY / 100 : 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -1087,7 +1087,7 @@ const width = args.WIDTH !== "" ? args.WIDTH / 50 : 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -1202,7 +1202,7 @@ const removeUnder = args.REMOVE; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -1249,7 +1249,7 @@ const a = Math.min(Math.max(args.A, 0), 100) * 2.55; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -1473,7 +1473,7 @@ const direction = args.DIRECT; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width + Math.abs(percentage) * 5; canvas.height = img.height + Math.abs(percentage) * 5; @@ -1752,12 +1752,17 @@ const imageElement = new Image(); const softness = this.softness; - return new Promise(resolve => { + return new Promise((resolve) => { imageElement.onload = () => { canvasElement.width = imageElement.width; canvasElement.height = imageElement.height; context.drawImage(imageElement, 0, 0); - const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); + const imageData = context.getImageData( + 0, + 0, + canvasElement.width, + canvasElement.height + ); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { @@ -1794,12 +1799,17 @@ const imageElement = new Image(); const softness = this.softness; - return new Promise(resolve => { + return new Promise((resolve) => { imageElement.onload = () => { canvasElement.width = imageElement.width; canvasElement.height = imageElement.height; context.drawImage(imageElement, 0, 0); - const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); + const imageData = context.getImageData( + 0, + 0, + canvasElement.width, + canvasElement.height + ); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { From 37a32c91d140808f0482f58c30c83089c642cf54 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:00:17 -0800 Subject: [PATCH 05/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 1212 ++++++++----------------- 1 file changed, 364 insertions(+), 848 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 5a5c51cfb4..0a9b44a6de 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -1,36 +1,35 @@ // Name: Image Effects // ID: imgEffectsSP // Description: Apply a variety of new effects to the data URI of Images or Costumes. -// By: SharkPool +// By: SharkPool -// Version V.1.4.0 +// Version V.1.5.0 (function (Scratch) { "use strict"; - if (!Scratch.extensions.unsandboxed) { - throw new Error("Image Effects extension must run unsandboxed"); - } + if (!Scratch.extensions.unsandboxed) throw new Error("Image Effects must run unsandboxed"); + Scratch.vm.extensionManager.loadExtensionURL("https://extensions.turbowarp.org/Lily/Skins.js"); const menuIconURI = - "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; +"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; function hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); - return [r, g, b]; + const a = hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 255; + return [r, g, b, a]; } class imgEffectsSP { constructor() { this.cutoutX = 0; this.cutoutY = 0; - this.scale = 100; + this.scale = [100, 100]; this.cutoutDirection = 90; this.softness = 10; } - getInfo() { return { id: "imgEffectsSP", @@ -39,481 +38,274 @@ color1: "#9966FF", color2: "#774DCB", blocks: [ - { - blockType: Scratch.BlockType.LABEL, - text: "Effects", - }, + { blockType: Scratch.BlockType.LABEL, text: "Effects" }, { opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, text: "convert [HEX] to [CHANNEL]", + hideFromPalette: true, // depreciated block arguments: { - HEX: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#FF0000", - }, - CHANNEL: { - type: Scratch.ArgumentType.STRING, - menu: "CHANNELS", - defaultValue: "R", - }, - }, + HEX: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } + } }, { opcode: "applyHueEffect", blockType: Scratch.BlockType.REPORTER, text: "apply hue [COLOR] to URI [SVG]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#FF0000", - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + } }, - "---", - { opcode: "deleteColor", blockType: Scratch.BlockType.REPORTER, text: "remove color [COLOR] from [DATA_URI]", arguments: { - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#FF0000", - }, - DATA_URI: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," } + } }, { opcode: "replaceColor", blockType: Scratch.BlockType.REPORTER, - text: "replace color [COLOR] with [REPLACE_COLOR] from [DATA_URI]", + text: "replace color [COLOR] with [REPLACE] from [DATA_URI]", arguments: { - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#FF0000", - }, - REPLACE_COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#00ff22", - }, - DATA_URI: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + REPLACE: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00ff00" }, + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," } + } }, { opcode: "setSoftness", blockType: Scratch.BlockType.COMMAND, text: "set softness of color detection to [AMT]%", arguments: { - AMT: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 10, - }, - }, + AMT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 } + } }, - "---", - { opcode: "applyEffect", blockType: Scratch.BlockType.REPORTER, text: "set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%", arguments: { - EFFECT: { - type: Scratch.ArgumentType.STRING, - menu: "EFFECTS", - defaultValue: "Saturation", - }, - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - PERCENTAGE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - }, + EFFECT: { type: Scratch.ArgumentType.STRING, menu: "EFFECTS" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 } + } }, { opcode: "applyBulgeEffect", blockType: Scratch.BlockType.REPORTER, text: "set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - STRENGTH: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - CENTER_X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - CENTER_Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + STRENGTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, + CENTER_X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + CENTER_Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } + } }, { opcode: "applyWaveEffect", blockType: Scratch.BlockType.REPORTER, text: "set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - AMPX: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - AMPY: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - FREQX: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 5, - }, - FREQY: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 5, - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + AMPX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, + AMPY: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, + FREQX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + FREQY: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } + } }, { opcode: "applyLineGlitchEffect", blockType: Scratch.BlockType.REPORTER, text: "set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - PERCENTAGE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - DIRECT: { - type: Scratch.ArgumentType.STRING, - menu: "POSITIONS", - defaultValue: "X", - }, - WIDTH: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 5, - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, + DIRECT: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" }, + WIDTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } + } }, { opcode: "applyAbberationEffect", blockType: Scratch.BlockType.REPORTER, text: "set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - PERCENTAGE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 5, - }, - COLOR1: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#ff0000", - }, - COLOR2: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#00f7ff", - }, - DIRECT: { - type: Scratch.ArgumentType.STRING, - menu: "POSITIONS", - defaultValue: "X", - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, + COLOR1: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + COLOR2: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00f7ff" }, + DIRECT: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" } + } }, - "---", - { opcode: "removeTransparencyEffect", blockType: Scratch.BlockType.REPORTER, text: "remove pixels from URI [SVG] [REMOVE] [THRESHOLD]% transparency", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - THRESHOLD: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 50, - }, - REMOVE: { - type: Scratch.ArgumentType.STRING, - menu: "REMOVAL", - defaultValue: "under", - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + THRESHOLD: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, + REMOVE: { type: Scratch.ArgumentType.STRING, menu: "REMOVAL" } + } }, { opcode: "applyEdgeOutlineEffect", blockType: Scratch.BlockType.REPORTER, text: "add outline to URI [SVG] with thickness [THICKNESS] and color [COLOR] opacity [A]%", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - THICKNESS: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 1, - }, - COLOR: { - type: Scratch.ArgumentType.COLOR, - defaultValue: "#FF0000", - }, - A: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - }, - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Clipping", + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + THICKNESS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + A: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + } }, + { blockType: Scratch.BlockType.LABEL, text: "Clipping" }, { opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, text: "clip [CUTOUT] from [MAIN]", arguments: { - MAIN: { - type: Scratch.ArgumentType.STRING, - defaultValue: "source-uri-here", - }, - CUTOUT: { - type: Scratch.ArgumentType.STRING, - defaultValue: "cutout-uri-here", - }, - }, + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-uri-here" }, + CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-uri-here" } + } }, { opcode: "setCutout", blockType: Scratch.BlockType.COMMAND, text: "set clipping position to x [X] y [Y]", arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } + } }, { opcode: "changeCutout", blockType: Scratch.BlockType.COMMAND, text: "change clipping position by x [X] y [Y]", arguments: { - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - }, + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } + } }, { opcode: "currentCut", blockType: Scratch.BlockType.REPORTER, text: "clipping [POS]", + disableMonitor: true, arguments: { - POS: { - type: Scratch.ArgumentType.STRING, - menu: "POSITIONS", - defaultValue: "X", - }, - }, + POS: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" } + } }, { opcode: "setScale", blockType: Scratch.BlockType.COMMAND, - text: "set clipping size to [SIZE]%", + text: "set clipping size to x [SIZE] y [Y]", arguments: { - SIZE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - }, + SIZE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + } }, { opcode: "changeScale", blockType: Scratch.BlockType.COMMAND, - text: "change clipping size by [SIZE]", + text: "change clipping size by x [SIZE] y [Y]", arguments: { - SIZE: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 10, - }, - }, + SIZE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } + } }, { opcode: "currentScale", blockType: Scratch.BlockType.REPORTER, - text: "clipping size", + text: "clipping size [POS]", + disableMonitor: true, + arguments: { + POS: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" } + } }, { opcode: "setDirection", blockType: Scratch.BlockType.COMMAND, text: "set clipping direction to [ANGLE]", arguments: { - ANGLE: { - type: Scratch.ArgumentType.ANGLE, - defaultValue: 90, - }, - }, + ANGLE: { type: Scratch.ArgumentType.ANGLE, defaultValue: 90 } + } }, { opcode: "changeDirection", blockType: Scratch.BlockType.COMMAND, text: "change clipping direction by [ANGLE]", arguments: { - ANGLE: { - type: Scratch.ArgumentType.ANGLE, - defaultValue: 15, - }, - }, + ANGLE: { type: Scratch.ArgumentType.ANGLE, defaultValue: 15 } + } }, { opcode: "currentDir", blockType: Scratch.BlockType.REPORTER, - text: "clipping direction", - }, - { - blockType: Scratch.BlockType.LABEL, - text: "Image Conversions", + text: "clipping direction" }, + { blockType: Scratch.BlockType.LABEL, text: "Image Conversions" }, { opcode: "svgToBitmap", blockType: Scratch.BlockType.REPORTER, text: "convert svg content [SVG] to bitmap with width [WIDTH] height [HEIGHT]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "", - }, - WIDTH: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - HEIGHT: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" }, + WIDTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + HEIGHT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + } }, { opcode: "convertImageToSVG", blockType: Scratch.BlockType.REPORTER, text: "convert bitmap URI [URI] to svg [TYPE]", arguments: { - URI: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "fileType", - defaultValue: "content", - }, - }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } + } }, { opcode: "makeSVGimage", blockType: Scratch.BlockType.REPORTER, - text: "make svg with image URI [URI] to svg [TYPE]", + text: "make new svg with image URI [URI] to svg [TYPE]", arguments: { - URI: { - type: Scratch.ArgumentType.STRING, - defaultValue: "data:,", - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "fileType", - defaultValue: "content", - }, - }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } + } }, { opcode: "audioToImage", blockType: Scratch.BlockType.REPORTER, text: "convert audio URI [AUDIO_URI] to PNG with width [W] and height [H]", arguments: { - AUDIO_URI: { - type: Scratch.ArgumentType.STRING, - defaultValue: "audio_uri_here", - }, - W: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - H: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 100, - }, - }, + AUDIO_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "audio_uri_here" }, + W: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, + H: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + } }, - "---", - { opcode: "skewSVG", blockType: Scratch.BlockType.REPORTER, text: "skew SVG content [SVG] at x [Y] y [X] as [TYPE]", arguments: { - SVG: { - type: Scratch.ArgumentType.STRING, - defaultValue: "", - }, - X: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - Y: { - type: Scratch.ArgumentType.NUMBER, - defaultValue: 0, - }, - TYPE: { - type: Scratch.ArgumentType.STRING, - menu: "fileType", - defaultValue: "content", - }, - }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" }, + X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } + } }, ], menus: { CHANNELS: { acceptReporters: true, - items: ["R", "G", "B"], + items: ["R", "G", "B"] }, POSITIONS: ["X", "Y"], REMOVAL: ["under", "over", "equal to"], @@ -521,38 +313,54 @@ EFFECTS: { acceptReporters: true, items: [ - "Saturation", - "Glitch", - "Chunk Glitch", - "Clip Glitch", - "Vignette", - "Ripple", - "Displacement", - "Posterize", - "Blur", - "Scanlines", - "Grain", - "Cubism", - ], + "Saturation", "Glitch", "Chunk Glitch", "Clip Glitch", + "Vignette", "Ripple", "Displacement", "Posterize", + "Blur", "Scanlines", "Grain", "Cubism", + ] }, }, }; } - convertHexToRGB(args) { - const hexColor = args.HEX; - const channel = args.CHANNEL; + setCutout(args) { + this.cutoutX = Scratch.Cast.toNumber(args.X); + this.cutoutY = Scratch.Cast.toNumber(args.Y); + } + changeCutout(args) { + this.cutoutX = this.cutoutX + Scratch.Cast.toNumber(args.X); + this.cutoutY = this.cutoutY + Scratch.Cast.toNumber(args.Y); + } + currentCut(args) { return args.POS === "X" ? this.cutoutX : this.cutoutY } + + setScale(args) { + this.scale[0] = Scratch.Cast.toNumber(args.SIZE); + this.scale[1] = Scratch.Cast.toNumber(args.Y); + } + changeScale(args) { + this.scale[0] = this.scale[0] + Scratch.Cast.toNumber(args.SIZE); + this.scale[1] = this.scale[1] + Scratch.Cast.toNumber(args.Y); + } + currentScale(args) { return this.scale[args.POS === "X" ? 0 : 1] } + + setDirection(args) { this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE) } + changeDirection(args) { + let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); + if (direction > 180) { direction = -180 + Scratch.Cast.toNumber(args.ANGLE) } + if (direction < -180) { direction = 180 + Scratch.Cast.toNumber(args.ANGLE) } + this.cutoutDirection = direction; + } + currentDir() { return this.cutoutDirection } - const r = parseInt(hexColor.substring(1, 3), 16); - const g = parseInt(hexColor.substring(3, 5), 16); - const b = parseInt(hexColor.substring(5, 7), 16); + setSoftness(args) { this.softness = Scratch.Cast.toNumber(args.AMT) } - if (channel === "R") { - return r; - } else if (channel === "G") { - return g; - } else if (channel === "B") { - return b; + convertHexToRGB(args) { + const hexColor = args.HEX; + if (args.CHANNEL === "R") { + return parseInt(hexColor.substring(1, 3), 16); + } else if (args.CHANNEL === "G") { + return parseInt(hexColor.substring(3, 5), 16); + } else { + return parseInt(hexColor.substring(5, 7), 16); } } @@ -560,12 +368,8 @@ return new Promise((resolve) => { const svgDataUri = args.SVG; const color = hexToRgb(args.COLOR); - const r = color[0]; - const g = color[1]; - const b = color[2]; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -573,16 +377,14 @@ ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyHue(imageData, r, g, b); + await this.applyHue(imageData, color[0], color[1], color[2]); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; img.src = svgDataUri; }); } - applyHue(imageData, r, g, b) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { @@ -592,13 +394,58 @@ } } + deleteColor(args) { + return this.replaceColor({ + COLOR : args.COLOR, + REPLACE : "#00000000", + DATA_URI : args.DATA_URI + }); + } + + replaceColor(args) { + const colRem = hexToRgb(args.COLOR); + const colRep = hexToRgb(args.REPLACE); + const dataURI = args.DATA_URI; + const canvasElement = document.createElement("canvas"); + const context = canvasElement.getContext("2d"); + const imageElement = new Image(); + const softness = this.softness; + return new Promise(resolve => { + imageElement.onload = () => { + canvasElement.width = imageElement.width; + canvasElement.height = imageElement.height; + context.drawImage(imageElement, 0, 0); + const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + if ( + r >= colRem[0] - softness && r <= colRem[0] + softness && + g >= colRem[1] - softness && g <= colRem[1] + softness && + b >= colRem[2] - softness && b <= colRem[2] + softness + ) { + data[i] = colRep[0]; + data[i + 1] = colRep[1]; + data[i + 2] = colRep[2]; + data[i + 3] = colRep[3]; + } + } + context.putImageData(imageData, 0, 0); + const newDataURI = canvasElement.toDataURL("image/png"); + resolve(newDataURI); + }; + imageElement.src = dataURI; + }); + } + applyEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE : 100; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -606,47 +453,13 @@ ctx.drawImage(img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const sEffect = args.EFFECT; - switch (sEffect) { - case "Glitch": - this.applyGlitch(imageData, percentage); - break; - case "Chunk Glitch": - this.applyChunkGlitch(imageData, percentage); - break; - case "Clip Glitch": - this.applyClipGlitch(imageData, percentage); - break; - case "Vignette": - this.applyVignette(imageData, percentage); - break; - case "Displacement": - this.applyDisplacement(imageData, percentage); - break; - case "Ripple": - this.applyRipple(imageData, percentage); - break; - case "Posterize": - this.applyPosterize(imageData, percentage); - break; - case "Blur": - this.applyBlur(imageData, percentage); - break; - case "Scanlines": - this.applyScanlines(imageData, percentage); - break; - case "Grain": - this.applyOldFilmGrain(imageData, percentage); - break; - case "Cubism": - this.applyCubism(imageData, percentage); - break; - default: - this.applySaturation(imageData, percentage); - break; + const effectFunction = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; + if (effectFunction && typeof effectFunction === "function") { + effectFunction(imageData, percentage); + } else { + this.applySaturation(imageData, percentage); } ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -667,55 +480,6 @@ } } - setCutout(args) { - this.cutoutX = Scratch.Cast.toNumber(args.X); - this.cutoutY = Scratch.Cast.toNumber(args.Y); - } - - changeCutout(args) { - this.cutoutX = this.cutoutX + Scratch.Cast.toNumber(args.X); - this.cutoutY = this.cutoutY + Scratch.Cast.toNumber(args.Y); - } - - setScale(args) { - this.scale = Scratch.Cast.toNumber(args.SIZE); - } - - changeScale(args) { - this.scale = this.scale + Scratch.Cast.toNumber(args.SIZE); - } - - setDirection(args) { - this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE); - } - - changeDirection(args) { - let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); - if (direction > 180) { - direction = -180 + Scratch.Cast.toNumber(args.ANGLE); - } - if (direction < -180) { - direction = 180 + Scratch.Cast.toNumber(args.ANGLE); - } - this.cutoutDirection = direction; - } - - currentCut(args) { - if (args.POS === "X") { - return this.cutoutX; - } else { - return this.cutoutY; - } - } - - currentScale() { - return this.scale; - } - - currentDir() { - return this.cutoutDirection; - } - clipImage(args) { return new Promise((resolve, reject) => { const mainImage = new Image(); @@ -726,19 +490,15 @@ canvas.width = mainImage.width; canvas.height = mainImage.height; const context = canvas.getContext("2d"); - const scaledWidth = cutoutImage.width + this.scale; - const scaledHeight = cutoutImage.height + this.scale; + const scaledWidth = cutoutImage.width + this.scale[0]; + const scaledHeight = cutoutImage.height + this.scale[1]; const cutX = this.cutoutX + mainImage.width / 2 - scaledWidth / 2; const cutY = this.cutoutY - mainImage.height / 2 + scaledHeight / 2; context.drawImage(mainImage, 0, 0); context.globalCompositeOperation = "destination-in"; - const rotationAngle = - ((this.cutoutDirection + 270) * Math.PI) / 180; - context.translate( - cutX + scaledWidth / 2, - cutY * -1 + scaledHeight / 2 - ); + const rotationAngle = ((this.cutoutDirection + 270) * Math.PI) / 180; + context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); context.rotate(rotationAngle); context.drawImage( cutoutImage, @@ -749,7 +509,6 @@ ); context.setTransform(1, 0, 0, 1, 0, 0); context.globalCompositeOperation = "source-over"; - const clippedDataURI = canvas.toDataURL("image/png"); resolve(clippedDataURI); }; @@ -764,20 +523,14 @@ const percent = Scratch.Cast.toNumber(percentage); for (let i = 0; i < data.length; i += 4) { const randomChance = Math.random() * 100; - if (randomChance <= percentage) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const negative = Math.random() < 0.5 ? -1 : 1; - const randomOffsetR = Math.random() * (percentage * 1.5 * negative); - const randomOffsetG = Math.random() * (percentage * 1.5 * negative); - const randomOffsetB = Math.random() * (percentage * 1.5 * negative); - - data[i] = (r + randomOffsetR) % 256; - data[i + 1] = (g + randomOffsetG) % 256; - data[i + 2] = (b + randomOffsetB) % 256; + const rndR = Math.random() * (percentage * 1.5 * negative); + const rndG = Math.random() * (percentage * 1.5 * negative); + const rndB = Math.random() * (percentage * 1.5 * negative); + data[i] = (data[i] + rndR) % 256; + data[i + 1] = (data[i + 1] + rndG) % 256; + data[i + 2] = (data[i + 2] + rndB) % 256; } } } @@ -786,41 +539,25 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const centerX = width / 2; const centerY = height / 2; - const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY); const percent = Scratch.Cast.toNumber(percentage); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; const distanceX = Math.abs(x - centerX); const distanceY = Math.abs(y - centerY); - const distance = Math.sqrt( - distanceX * distanceX + distanceY * distanceY - ); - let vignetteAmount = ""; + const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); + let vigAMT = ""; if (percent < 0) { - vignetteAmount = 1 - (distance / maxDistance) * (percent / 100); + vigAMT = 1 - (distance / maxDistance) * (percent / 100); } else { - vignetteAmount = - ((maxDistance - distance) / maxDistance) * (percent / 100); + vigAMT = ((maxDistance - distance) / maxDistance) * (percent / 100); } - - data[index] = Math.max( - 0, - Math.min(255, data[index] * vignetteAmount) - ); - data[index + 1] = Math.max( - 0, - Math.min(255, data[index + 1] * vignetteAmount) - ); - data[index + 2] = Math.max( - 0, - Math.min(255, data[index + 2] * vignetteAmount) - ); + data[index] = Math.max(0, Math.min(255, data[index] * vigAMT)); + data[index + 1] = Math.max(0, Math.min(255, data[index + 1] * vigAMT)); + data[index + 2] = Math.max(0, Math.min(255, data[index + 2] * vigAMT)); } } } @@ -831,24 +568,18 @@ const height = imageData.height; const centerX = width / 2; const centerY = height / 2; - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; const dx = x - centerX; const dy = y - centerY; const distance = Math.sqrt(dx * dx + dy * dy); - - const offset = - Math.sin(distance * (percentage / 100)) * (percentage / 100); + const offset = Math.sin(distance * (percentage / 100)) * (percentage / 100); const sourceX = Math.floor(x + offset); const sourceY = Math.floor(y); - if ( - sourceX >= 0 && - sourceX < width && - sourceY >= 0 && - sourceY < height + sourceX >= 0 && sourceX < width && + sourceY >= 0 && sourceY < height ) { const sourceIndex = (sourceY * width + sourceX) * 4; if (data[sourceIndex + 3] > 0) { @@ -866,29 +597,20 @@ } } - applyDisplacement(imageData, displacementAmount) { + applyDisplacement(imageData, dispAmt) { const data = imageData.data; const width = imageData.width; const height = imageData.height; const newData = new Uint8ClampedArray(data.length); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const srcX = - x + - Math.floor( - Math.random() * displacementAmount * 2 - displacementAmount - ); - const srcY = - y + - Math.floor( - Math.random() * displacementAmount * 2 - displacementAmount - ); - + const srcX = x + + Math.floor(Math.random() * dispAmt * 2 - dispAmt); + const srcY = y + + Math.floor(Math.random() * dispAmt * 2 - dispAmt); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; - newData[dstIndex] = data[srcIndex]; newData[dstIndex + 1] = data[srcIndex + 1]; newData[dstIndex + 2] = data[srcIndex + 2]; @@ -902,17 +624,13 @@ applyPosterize(imageData, percentage) { const data = imageData.data; const numLevels = Math.max(percentage / 10, 1); - for (let i = 0; i < data.length; i += 4) { data[i] = - Math.round((data[i] * (numLevels - 1)) / 255) * - (255 / (numLevels - 1)); + Math.round((data[i] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); data[i + 1] = - Math.round((data[i + 1] * (numLevels - 1)) / 255) * - (255 / (numLevels - 1)); + Math.round((data[i + 1] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); data[i + 2] = - Math.round((data[i + 2] * (numLevels - 1)) / 255) * - (255 / (numLevels - 1)); + Math.round((data[i + 2] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); } } @@ -922,9 +640,8 @@ let centerX = args.CENTER_X !== "" ? args.CENTER_X / 100 : 0; let centerY = args.CENTER_Y !== "" ? args.CENTER_Y / -100 : 0; const strength = args.STRENGTH !== "" ? args.STRENGTH / 100 : 0; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -932,11 +649,9 @@ centerY = centerY + img.height / 200; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyBulge(imageData, centerX, centerY, strength); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -949,7 +664,6 @@ const width = imageData.width; const height = imageData.height; const newData = new Uint8ClampedArray(data.length); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const dx = (x / width - centerX) * 2; @@ -958,7 +672,6 @@ const bulge = Math.pow(distance, strength); const srcX = Math.floor(x + dx * bulge * width - dx * width); const srcY = Math.floor(y + dy * bulge * height - dy * height); - if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; @@ -979,25 +692,16 @@ const amplitudeY = args.AMPY !== "" ? args.AMPY / 10 : 0; const frequencyX = args.FREQX !== "" ? args.FREQX / 100 : 0; const frequencyY = args.FREQY !== "" ? args.FREQY / 100 : 0; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyWave( - imageData, - amplitudeX, - amplitudeY, - frequencyX, - frequencyY - ); + this.applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -1010,16 +714,13 @@ const width = imageData.width; const height = imageData.height; const newData = new Uint8ClampedArray(data.length); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const srcX = Math.floor(x + amplitudeX * Math.sin(y * frequencyX)); const srcY = Math.floor(y + amplitudeY * Math.sin(x * frequencyY)); - if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; - newData[dstIndex] = data[srcIndex]; newData[dstIndex + 1] = data[srcIndex + 1]; newData[dstIndex + 2] = data[srcIndex + 2]; @@ -1035,7 +736,6 @@ const width = imageData.width; const height = imageData.height; const radius = percentage > 1 ? Math.floor((percentage / 100) * 10) : 0; - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { let r = 0, @@ -1043,20 +743,15 @@ b = 0, a = 0, count = 0; - for (let ky = -radius; ky <= radius; ky++) { for (let kx = -radius; kx <= radius; kx++) { const offsetX = x + kx; const offsetY = y + ky; - if ( - offsetX >= 0 && - offsetX < width && - offsetY >= 0 && - offsetY < height + offsetX >= 0 && offsetX < width && + offsetY >= 0 && offsetY < height ) { const pixelIndex = (offsetY * width + offsetX) * 4; - r += data[pixelIndex]; g += data[pixelIndex + 1]; b += data[pixelIndex + 2]; @@ -1065,7 +760,6 @@ } } } - const pixelIndex = (y * width + x) * 4; if (a === 0) { data[pixelIndex + 3] = a / count; @@ -1085,19 +779,16 @@ const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE / 100 : 0; const direction = args.DIRECT; const width = args.WIDTH !== "" ? args.WIDTH / 50 : 0; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyLineGlitch(imageData, percentage, direction, width); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -1110,27 +801,21 @@ const imgWidth = imageData.width; const imgHeight = imageData.height; const numLines = Math.floor(imgHeight * percentage); - for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { const linePosition = Math.floor(Math.random() * imgHeight); const lineStart = linePosition - Math.floor(width / 2); const lineEnd = lineStart + width; - if (direction === "Y") { for (let y = 0; y < imgHeight; y++) { for (let x = lineStart; x < lineEnd; x++) { const srcX = x; const srcY = linePosition; - if ( - srcX >= 0 && - srcX < imgWidth && - srcY >= 0 && - srcY < imgHeight + srcX >= 0 && srcX < imgWidth && + srcY >= 0 && srcY < imgHeight ) { const srcIndex = (srcY * imgWidth + srcX) * 4; const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; data[dstIndex + 1] = data[srcIndex + 1]; data[dstIndex + 2] = data[srcIndex + 2]; @@ -1143,16 +828,12 @@ for (let x = 0; x < imgWidth; x++) { const srcX = linePosition; const srcY = y; - if ( - srcX >= 0 && - srcX < imgWidth && - srcY >= 0 && - srcY < imgHeight + srcX >= 0 && srcX < imgWidth && + srcY >= 0 && srcY < imgHeight ) { const srcIndex = (srcY * imgWidth + srcX) * 4; const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; data[dstIndex + 1] = data[srcIndex + 1]; data[dstIndex + 2] = data[srcIndex + 2]; @@ -1170,21 +851,17 @@ const imgWidth = imageData.width; const imgHeight = imageData.height; const numLines = Math.floor(imgWidth * 1); - for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { const linePosition = Math.floor(Math.random() * imgHeight); const lineStart = linePosition - Math.floor(newWidth / 2); const lineEnd = lineStart + newWidth; - for (let y = 0; y < imgHeight; y++) { for (let x = lineStart; x < lineEnd; x++) { const srcX = linePosition; const srcY = y; - if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { const srcIndex = (srcY * imgWidth + srcX) * 4; const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; data[dstIndex + 1] = data[srcIndex + 1]; data[dstIndex + 2] = data[srcIndex + 2]; @@ -1200,19 +877,16 @@ const svgDataUri = args.SVG; const threshold = args.THRESHOLD !== "" ? args.THRESHOLD / 100 : 0; const removeUnder = args.REMOVE; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyTransparencyRemoval(imageData, threshold, removeUnder); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -1223,7 +897,6 @@ applyTransparencyRemoval(imageData, threshold, removeUnder) { const data = imageData.data; const pixelCount = data.length / 4; - for (let i = 0; i < pixelCount; i++) { const alpha = data[i * 4 + 3] / 255; if ( @@ -1243,23 +916,17 @@ const svgDataUri = args.SVG; const thickness = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); const color = hexToRgb(args.COLOR); - const r = color[0]; - const g = color[1]; - const b = color[2]; const a = Math.min(Math.max(args.A, 0), 100) * 2.55; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyOutline(imageData, thickness, r, g, b, a); + this.applyOutline(imageData, thickness, color[0], color[1], color[2], a); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -1271,29 +938,24 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const outlineColor = [r, g, b, a]; const copyData = new Uint8ClampedArray(data); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; const alpha = data[index + 3]; - if (alpha < 255) { for (let dy = -thickness; dy <= thickness; dy++) { for (let dx = -thickness; dx <= thickness; dx++) { const nx = x + dx; const ny = y + dy; - if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const neighborIndex = (ny * width + nx) * 4; const neighborAlpha = copyData[neighborIndex + 3]; - if (neighborAlpha === 255) { - data[index] = outlineColor[0]; - data[index + 1] = outlineColor[1]; - data[index + 2] = outlineColor[2]; - data[index + 3] = outlineColor[3]; + data[index] = r; + data[index + 1] = g; + data[index + 2] = b; + data[index + 3] = a; break; } } @@ -1309,27 +971,20 @@ const width = imageData.width; const height = imageData.height; const percent = percentage / 100; - const numPixelsToEnlarge = Math.floor((percent / 100) * (width * height)); const maxEnlargeFactor = 1.5 + percent / 200; - for (let i = 0; i < numPixelsToEnlarge; i++) { const x = Math.floor(Math.random() * width); const y = Math.floor(Math.random() * height); const index = (y * width + x) * 4; - const enlargeFactor = 1 + Math.random() * maxEnlargeFactor; - const blurRadius = Math.floor(enlargeFactor * 4); - for (let offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) { for (let offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) { const newX = x + offsetX; const newY = y + offsetY; - if (newX >= 0 && newX < width && newY >= 0 && newY < height) { const newIndex = (newY * width + newX) * 4; - data[newIndex] = data[index]; data[newIndex + 1] = data[index + 1]; data[newIndex + 2] = data[index + 2]; @@ -1344,40 +999,27 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const scanlineHeight = Math.floor(height / 100); - const percent = percentage / 100; - for (let y = 0; y < height; y++) { - if (Math.random() < percent) { - const scanlineBrightness = Math.random() * (percentage / 2); - + if (Math.random() < percentage / 100) { + const scanBright = Math.random() * (percentage / 2); for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; - data[index] = Math.min(data[index] + scanlineBrightness, 255); - data[index + 1] = Math.min( - data[index + 1] + scanlineBrightness, - 255 - ); - data[index + 2] = Math.min( - data[index + 2] + scanlineBrightness, - 255 - ); + data[index] = Math.min(data[index] + scanBright, 255); + data[index + 1] = Math.min(data[index + 1] + scanBright, 255); + data[index + 2] = Math.min(data[index + 2] + scanBright, 255); } } } } - applyOldFilmGrain(imageData, percentage) { + applyGrain(imageData, percentage) { const data = imageData.data; const width = imageData.width; const height = imageData.height; - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; - const rand = Math.random(); - - if (rand < percentage) { + if (Math.random() < percentage) { const grain = Math.floor(Math.random() * percentage); data[index] += grain; data[index + 1] += grain; @@ -1391,11 +1033,7 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const percent = - percentage === 0 || percentage === "" - ? 1 - : Math.abs(Scratch.Cast.toNumber(percentage)); - + const percent = percentage === 0 || percentage === "" ? 1 : Math.abs(Scratch.Cast.toNumber(percentage)); for (let y = 0; y < height; y += percent) { for (let x = 0; x < width; x += percent) { const startX = x; @@ -1403,7 +1041,6 @@ const startY = y; const endY = Math.min(y + percent, height); const avgColor = [0, 0, 0]; - for (let j = startY; j < endY; j++) { for (let i = startX; i < endX; i++) { const index = (j * width + i) * 4; @@ -1412,12 +1049,10 @@ avgColor[2] += data[index + 2]; } } - const totalPixels = (endX - startX) * (endY - startY); avgColor[0] /= totalPixels; avgColor[1] /= totalPixels; avgColor[2] /= totalPixels; - for (let j = startY; j < endY; j++) { for (let i = startX; i < endX; i++) { const index = (j * width + i) * 4; @@ -1430,70 +1065,24 @@ } } - svgToBitmap(args) { - const svgContent = args.SVG; - const width = Math.abs(Scratch.Cast.toNumber(args.WIDTH)); - const height = Math.abs(Scratch.Cast.toNumber(args.HEIGHT)); - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - - if (args.WIDTH < 0) { - ctx.translate(width, 0); - ctx.scale(-1, 1); - } - if (args.HEIGHT < 0) { - ctx.translate(0, height); - ctx.scale(1, -1); - } - ctx.drawImage(img, 0, 0, width, height); - const imageData = ctx.getImageData(0, 0, width, height); - const newCanvas = document.createElement("canvas"); - newCanvas.width = width; - newCanvas.height = height; - const newCtx = newCanvas.getContext("2d"); - newCtx.putImageData(imageData, 0, 0); - const dataUri = newCanvas.toDataURL(); - resolve(dataUri); - }; - img.src = `data:image/svg+xml;base64,${btoa(svgContent)}`; - }); - } - applyAbberationEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; const percentage = args.PERCENTAGE; - const color1 = args.COLOR1; - const color2 = args.COLOR2; - const direction = args.DIRECT; - const img = new Image(); - img.onload = () => { + img.onload = async () => { const canvas = document.createElement("canvas"); canvas.width = img.width + Math.abs(percentage) * 5; canvas.height = img.height + Math.abs(percentage) * 5; const ctx = canvas.getContext("2d"); ctx.drawImage( - img, - Math.abs(percentage) * 2.5, - Math.abs(percentage) * 2.5 + img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5 ); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyChromaticAberration( - imageData, - color1, - color2, - percentage, - direction - ); + imageData, args.COLOR1, args.COLOR2, + percentage, args.DIRECT); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); resolve(modifiedDataUrl); }; @@ -1507,16 +1096,13 @@ let height = imageData.height; const copy1 = new Uint8ClampedArray(data.length); const copy2 = new Uint8ClampedArray(data.length); - const hexToRGB = (hex) => [ parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16), ]; - const rgb1 = hexToRGB(color1); const rgb2 = hexToRGB(color2); - for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const srcIndex = (y * width + x) * 4; @@ -1524,9 +1110,7 @@ const g = data[srcIndex + 1]; const b = data[srcIndex + 2]; const a = data[srcIndex + 3] / 1; - let newX1, newY1, newX2, newY2; - if (direction === "X") { newX1 = x + Math.floor((width / 2) * (percentage / 100)); newY1 = y; @@ -1538,12 +1122,10 @@ newX2 = x; newY2 = y - Math.floor((height / 2) * (percentage / 100)); } - newX1 = Math.max(0, Math.min(width - 1, newX1)); newY1 = Math.max(0, Math.min(height - 1, newY1)); newX2 = Math.max(0, Math.min(width - 1, newX2)); newY2 = Math.max(0, Math.min(height - 1, newY2)); - const newR1 = data[(newY1 * width + newX1) * 4]; const newG1 = data[(newY1 * width + newX1) * 4 + 1]; const newB1 = data[(newY1 * width + newX1) * 4 + 2]; @@ -1551,7 +1133,6 @@ const newR2 = data[(newY2 * width + newX2) * 4]; const newG2 = data[(newY2 * width + newX2) * 4 + 1]; const newB2 = data[(newY2 * width + newX2) * 4 + 2]; - const leftColor = [ (rgb1[0] * r) / 255, (rgb1[1] * g) / 255, @@ -1562,7 +1143,6 @@ (rgb2[1] * g) / 255, (rgb2[2] * b) / 255, ]; - const leftIndex = (newY1 * width + newX1) * 4; const rightIndex = (newY2 * width + newX2) * 4; @@ -1577,27 +1157,54 @@ copy2[rightIndex + 3] = a; } } - for (let i = 0; i < data.length; i++) { - data[i] = Math.max( - 0, - Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2) - ); + data[i] = Math.max(0, Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2)); } } + svgToBitmap(args) { + const svgContent = args.SVG; + const width = Math.abs(Scratch.Cast.toNumber(args.WIDTH)); + const height = Math.abs(Scratch.Cast.toNumber(args.HEIGHT)); + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (args.WIDTH < 0) { + ctx.translate(width, 0); + ctx.scale(-1, 1); + } + if (args.HEIGHT < 0) { + ctx.translate(0, height); + ctx.scale(1, -1); + } + ctx.drawImage(img, 0, 0, width, height); + const imageData = ctx.getImageData(0, 0, width, height); + const newCanvas = document.createElement("canvas"); + newCanvas.width = width; + newCanvas.height = height; + const newCtx = newCanvas.getContext("2d"); + newCtx.putImageData(imageData, 0, 0); + const dataUri = newCanvas.toDataURL(); + resolve(dataUri); + }; + img.src = `data:image/svg+xml;base64,${btoa(svgContent)}`; + }); + } + convertImageToSVG(args) { return new Promise((resolve) => { const img = new Image(); img.src = args.URI; - 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, img.width, img.height); - const svg = document.createElementNS( "http://www.w3.org/2000/svg", "svg" @@ -1612,20 +1219,14 @@ `0,0,${img.width.toFixed(5)},${img.height.toFixed(5)}` ); const mergedColors = new Map(); - for (let y = 0; y < img.height; y++) { for (let x = 0; x < img.width; x++) { const colorData = ctx.getImageData(x, y, 1, 1).data; const alpha = colorData[3]; - - if (alpha === 0) { - continue; - } - + if (alpha === 0) continue; const color = `rgb(${colorData[0]}, ${colorData[1]}, ${colorData[2]})`; const rightColorData = ctx.getImageData(x + 1, y, 1, 1).data; const rightColor = `rgb(${rightColorData[0]}, ${rightColorData[1]}, ${rightColorData[2]})`; - if (color === rightColor) { const mergedPixel = mergedColors.get(color) || { x1: x, @@ -1652,7 +1253,6 @@ (mergedPixel.y2 - mergedPixel.y1 + 1).toFixed(5) ); rect.setAttribute("fill", colorKey); - svg.appendChild(rect); }); mergedColors.clear(); @@ -1660,30 +1260,35 @@ } } let svgString = new XMLSerializer().serializeToString(svg); - if (args.TYPE === "dataURI") { - svgString = `data:image/svg+xml;base64,${btoa(svgString)}`; - } + if (args.TYPE === "dataURI") svgString = `data:image/svg+xml;base64,${btoa(svgString)}`; resolve(svgString); }; }); } - makeSVGimage(args) { - const img = new Image(); - img.src = args.URI; - const width = img.width; - const height = img.height; - let base = ``; - if (args.TYPE === "dataURI") { - base = `data:image/svg+xml;base64,${btoa(base)}`; + async makeSVGimage(args) { + if (args.URI.startsWith("data:image/")) { + return await new Promise((resolve, reject) => { + // eslint-disable-next-line + const img = new Image(); + img.onload = () => { + const width = img.width; + const height = img.height; + const svg = ` + + `; + resolve(args.TYPE === "dataURI" ? `data:image/svg+xml;base64,${btoa(svg)}` : svg); + }; + img.onerror = reject; + img.src = args.URI; + }); + } else { + return args.URI; } - return base; } audioToImage(args) { @@ -1694,7 +1299,6 @@ const ctx = canvas.getContext("2d"); canvas.width = imageWidth; canvas.height = imageHeight; - for (let i = 0; i < audioURI.length; i++) { const charCode = audioURI.charCodeAt(i); const red = (charCode * 2) % 256; @@ -1708,137 +1312,49 @@ } skewSVG(args) { - let SVG = args.SVG; - const parser = new DOMParser(); - const doc = parser.parseFromString(SVG, "image/svg+xml"); - const svgElement = doc.documentElement; - const originalWidth = parseFloat(svgElement.getAttribute("width")); - const originalHeight = parseFloat(svgElement.getAttribute("height")); - const newTransform = `matrix(1, ${args.X / 100}, ${ - args.Y / 100 - }, 1, ${Math.abs(args.X)}, ${Math.abs(args.Y)})`; - - svgElement.setAttribute("transform", newTransform); - - const skewX = Math.abs((args.X / 100) * originalWidth); - const skewY = Math.abs((args.Y / 100) * originalHeight); - const newViewBoxWidth = originalWidth + 2 * skewX; - const newViewBoxHeight = originalHeight + 2 * skewY; - let newViewBoxX = (newViewBoxWidth - originalWidth) / 2; - newViewBoxX = newViewBoxX < 0 || args.Y < 0 ? 0 : newViewBoxX; - let newViewBoxY = (newViewBoxHeight - originalHeight) / 2; - newViewBoxY = newViewBoxY < 0 || args.X < 0 ? 0 : newViewBoxY; - let offsetX = args.X < 0 ? 1 : 0; - let offsetY = args.Y < 0 ? 1 : 0; - const newViewBox = `${newViewBoxX} ${newViewBoxY} ${ - newViewBoxWidth - Math.abs(args.X * offsetX) - } ${newViewBoxHeight - Math.abs(args.Y * offsetY)}`; - svgElement.setAttribute("viewBox", newViewBox); - - const serializer = new XMLSerializer(); - SVG = serializer.serializeToString(svgElement); - if (args.TYPE === "dataURI") { - SVG = `data:image/svg+xml;base64,${btoa(SVG)}`; - } - return SVG; - } - - deleteColor(args) { - const hexColorToBeRemoved = args.COLOR; - const colorToBeRemoved = hexToRgb(hexColorToBeRemoved); - const dataURI = args.DATA_URI; - const canvasElement = document.createElement("canvas"); - const context = canvasElement.getContext("2d"); - const imageElement = new Image(); - const softness = this.softness; - - return new Promise((resolve) => { - imageElement.onload = () => { - canvasElement.width = imageElement.width; - canvasElement.height = imageElement.height; - context.drawImage(imageElement, 0, 0); - const imageData = context.getImageData( - 0, - 0, - canvasElement.width, - canvasElement.height - ); - const data = imageData.data; - - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - if ( - r >= colorToBeRemoved[0] - softness && - r <= colorToBeRemoved[0] + softness && - g >= colorToBeRemoved[1] - softness && - g <= colorToBeRemoved[1] + softness && - b >= colorToBeRemoved[2] - softness && - b <= colorToBeRemoved[2] + softness - ) { - data[i + 3] = 0; - } - } - context.putImageData(imageData, 0, 0); - const newDataURI = canvasElement.toDataURL("image/png"); - resolve(newDataURI); - }; - imageElement.src = dataURI; - }); - } - - replaceColor(args) { - const hexColorToBeRemoved = args.COLOR; - const ColorReplaced = args.REPLACE_COLOR; - const colorToBeRemoved = hexToRgb(hexColorToBeRemoved); - const colorToBeReplaced = hexToRgb(ColorReplaced); - const dataURI = args.DATA_URI; - const canvasElement = document.createElement("canvas"); - const context = canvasElement.getContext("2d"); - const imageElement = new Image(); - const softness = this.softness; - - return new Promise((resolve) => { - imageElement.onload = () => { - canvasElement.width = imageElement.width; - canvasElement.height = imageElement.height; - context.drawImage(imageElement, 0, 0); - const imageData = context.getImageData( - 0, - 0, - canvasElement.width, - canvasElement.height + let svg = this.updateView(args.SVG, Math.abs(args.X) + Math.abs(args.Y)); + const widthMatch = /width="([^"]*)"/.exec(svg); + const heightMatch = /height="([^"]*)"/.exec(svg); + if (widthMatch && heightMatch) { + const width = parseFloat(widthMatch[1]); + const height = parseFloat(heightMatch[1]); + let transform = ""; + if (svg.includes("style=\"transform-origin: center; transform:")) { + svg = svg.replace(/(style="[^"]*transform:[^"]*)/, `$1 skew(${args.Y}deg, ${args.X}deg)`); + } else { + svg = svg.replace( + `width="${width}" height="${height}"`, + `width="${width}" height="${height}" style="transform-origin: center; transform: skew(${args.Y}deg, ${args.X}deg)"` ); - const data = imageData.data; - - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - if ( - r >= colorToBeRemoved[0] - softness && - r <= colorToBeRemoved[0] + softness && - g >= colorToBeRemoved[1] - softness && - g <= colorToBeRemoved[1] + softness && - b >= colorToBeRemoved[2] - softness && - b <= colorToBeRemoved[2] + softness - ) { - data[i] = colorToBeReplaced[0]; - data[i + 1] = colorToBeReplaced[1]; - data[i + 2] = colorToBeReplaced[2]; - } - } - context.putImageData(imageData, 0, 0); - const newDataURI = canvasElement.toDataURL("image/png"); - resolve(newDataURI); - }; - imageElement.src = dataURI; - }); + } + const currentTransform = /transform="([^"]*)"/.exec(svg); + const existingTransform = currentTransform ? currentTransform[1] : ""; + const newTransform = existingTransform ? `${existingTransform} ${transform}` : transform; + svg = svg.replace(/transform="([^"]*)"/, `transform="${newTransform}"`); + if (args.TYPE === "dataURI") svg = `data:image/svg+xml;base64,${btoa(svg)}`; + } + return svg; } - setSoftness(args) { - this.softness = Scratch.Cast.toNumber(args.AMT); + updateView(svg, amt) { + let values; + const viewBoxMatch = svg.match(/viewBox="([^"]+)"/); + let viewBoxValues = -1; + if (viewBoxMatch) viewBoxValues = viewBoxMatch[1].split(/\s*,\s*/).map(parseFloat); + const translateMatch = svg.match(/ Scratch.Cast.toNumber(item)); + amt = Scratch.Cast.toNumber(amt); + if (values.length > 3) { + svg = svg.replace(/viewBox="([^"]+)"/, `viewBox="${values[0]},${values[1]},${values[2] + (amt * 2)},${values[3] + (amt * 2)}"`); + svg = svg.replace(/width="([^"]+)"/, `width="${values[2] + (amt * 2)}"`); + svg = svg.replace(/height="([^"]+)"/, `height="${values[3] + (amt * 2)}"`); + svg = svg.replace(/ Date: Sun, 10 Dec 2023 22:21:07 -0800 Subject: [PATCH 06/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 0a9b44a6de..2ae5cd12f4 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -443,7 +443,7 @@ applyEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE : 100; + const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) || 100; const img = new Image(); img.onload = async () => { const canvas = document.createElement("canvas"); @@ -455,7 +455,7 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const effectFunction = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; if (effectFunction && typeof effectFunction === "function") { - effectFunction(imageData, percentage); + await effectFunction(imageData, percentage); } else { this.applySaturation(imageData, percentage); } @@ -637,11 +637,11 @@ applyBulgeEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - let centerX = args.CENTER_X !== "" ? args.CENTER_X / 100 : 0; - let centerY = args.CENTER_Y !== "" ? args.CENTER_Y / -100 : 0; - const strength = args.STRENGTH !== "" ? args.STRENGTH / 100 : 0; + let centerX = Scratch.Cast.toNumber(args.CENTER_X) / 100 || 0; + let centerY = Scratch.Cast.toNumber(args.CENTER_Y) / -100 || 0; + const strength = Scratch.Cast.toNumber(args.STRENGTH) / 100 || 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -688,12 +688,12 @@ applyWaveEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const amplitudeX = args.AMPX !== "" ? args.AMPX / 10 : 0; - const amplitudeY = args.AMPY !== "" ? args.AMPY / 10 : 0; - const frequencyX = args.FREQX !== "" ? args.FREQX / 100 : 0; - const frequencyY = args.FREQY !== "" ? args.FREQY / 100 : 0; + const amplitudeX = Scratch.Cast.toNumber(args.AMPX) / 10 || 0; + const amplitudeY = Scratch.Cast.toNumber(args.AMPY) / 10 || 0; + const frequencyX = Scratch.Cast.toNumber(args.FREQX) / 100 || 0; + const frequencyY = Scratch.Cast.toNumber(args.FREQY) / 100 || 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -776,11 +776,11 @@ applyLineGlitchEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const percentage = args.PERCENTAGE !== "" ? args.PERCENTAGE / 100 : 0; + const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) / 100 || 0; const direction = args.DIRECT; - const width = args.WIDTH !== "" ? args.WIDTH / 50 : 0; + const width = Scratch.Cast.toNumber(args.WIDTH) / 50 || 0; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -875,10 +875,10 @@ removeTransparencyEffect(args) { return new Promise((resolve) => { const svgDataUri = args.SVG; - const threshold = args.THRESHOLD !== "" ? args.THRESHOLD / 100 : 0; + const threshold = Scratch.Cast.toNumber(args.THRESHOLD) / 100 || 0; const removeUnder = args.REMOVE; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; @@ -918,7 +918,7 @@ const color = hexToRgb(args.COLOR); const a = Math.min(Math.max(args.A, 0), 100) * 2.55; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; From eafcc15fbceeb018456b8c76777ca72d8e8e2a97 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:24:29 -0800 Subject: [PATCH 07/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 2ae5cd12f4..b33cb3cff9 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -1070,7 +1070,7 @@ const svgDataUri = args.SVG; const percentage = args.PERCENTAGE; const img = new Image(); - img.onload = async () => { + img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width + Math.abs(percentage) * 5; canvas.height = img.height + Math.abs(percentage) * 5; From a8e0773521402c0fb76e51f315617ce1c2b211a2 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Thu, 14 Dec 2023 20:34:36 -0800 Subject: [PATCH 08/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 848 ++++++++++++-------------- 1 file changed, 397 insertions(+), 451 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index b33cb3cff9..a0727b3d83 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.1.5.0 +// Version V.2.0.0 (function (Scratch) { "use strict"; @@ -24,8 +24,7 @@ class imgEffectsSP { constructor() { - this.cutoutX = 0; - this.cutoutY = 0; + this.cutPos = [0, 0]; this.scale = [100, 100]; this.cutoutDirection = 90; this.softness = 10; @@ -54,7 +53,7 @@ blockType: Scratch.BlockType.REPORTER, text: "apply hue [COLOR] to URI [SVG]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } } }, @@ -65,7 +64,7 @@ text: "remove color [COLOR] from [DATA_URI]", arguments: { COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, - DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," } + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } }, { @@ -75,7 +74,7 @@ arguments: { COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, REPLACE: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00ff00" }, - DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," } + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } }, { @@ -93,7 +92,7 @@ text: "set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%", arguments: { EFFECT: { type: Scratch.ArgumentType.STRING, menu: "EFFECTS" }, - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 } } }, @@ -102,7 +101,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, STRENGTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, CENTER_X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, CENTER_Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } @@ -113,7 +112,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, AMPX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, AMPY: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, FREQX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, @@ -125,7 +124,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, DIRECT: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" }, WIDTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } @@ -136,7 +135,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, COLOR1: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, COLOR2: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00f7ff" }, @@ -149,7 +148,7 @@ blockType: Scratch.BlockType.REPORTER, text: "remove pixels from URI [SVG] [REMOVE] [THRESHOLD]% transparency", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, THRESHOLD: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, REMOVE: { type: Scratch.ArgumentType.STRING, menu: "REMOVAL" } } @@ -159,7 +158,7 @@ blockType: Scratch.BlockType.REPORTER, text: "add outline to URI [SVG] with thickness [THICKNESS] and color [COLOR] opacity [A]%", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, THICKNESS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, A: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } @@ -171,8 +170,8 @@ blockType: Scratch.BlockType.REPORTER, text: "clip [CUTOUT] from [MAIN]", arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-uri-here" }, - CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-uri-here" } + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, + CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } } }, { @@ -250,6 +249,37 @@ blockType: Scratch.BlockType.REPORTER, text: "clipping direction" }, + { blockType: Scratch.BlockType.LABEL, text: "Pixels" }, + { + opcode: "numPixels", + blockType: Scratch.BlockType.REPORTER, + text: "number of pixels [TYPE] in [URI]", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + TYPE: { type: Scratch.ArgumentType.STRING, menu: "PIXELTYPE" } + } + }, + { + opcode: "setPixel", + blockType: Scratch.BlockType.REPORTER, + text: "set color of pixel #[NUM] to [COLOR] in [URI]", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + } + }, + { + opcode: "setPixels", + blockType: Scratch.BlockType.REPORTER, + text: "set color of pixels from #[NUM] to [NUM2] to [COLOR] in [URI]", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, + NUM2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + } + }, { blockType: Scratch.BlockType.LABEL, text: "Image Conversions" }, { opcode: "svgToBitmap", @@ -266,7 +296,7 @@ blockType: Scratch.BlockType.REPORTER, text: "convert bitmap URI [URI] to svg [TYPE]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } } }, @@ -275,39 +305,65 @@ blockType: Scratch.BlockType.REPORTER, text: "make new svg with image URI [URI] to svg [TYPE]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "data:," }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } } }, + { + opcode: "upscaleImage", + blockType: Scratch.BlockType.REPORTER, + text: "upscale image URI [URI] by [NUM] %", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } + } + }, + "---", + { + opcode: "stretchImg", + blockType: Scratch.BlockType.REPORTER, + text: "stretch URI [URI] to width [W] height [H]", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + W: { type: Scratch.ArgumentType.NUMBER, defaultValue: 200 }, + H: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + } + }, + "---", { opcode: "audioToImage", blockType: Scratch.BlockType.REPORTER, - text: "convert audio URI [AUDIO_URI] to PNG with width [W] and height [H]", + text: "convert audio URI [AUDIO_URI] to PNG with width [W] height [H]", arguments: { AUDIO_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "audio_uri_here" }, W: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 }, H: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } } }, - "---", { opcode: "skewSVG", blockType: Scratch.BlockType.REPORTER, text: "skew SVG content [SVG] at x [Y] y [X] as [TYPE]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" }, X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } } }, + { + opcode: "removeThorns", + blockType: Scratch.BlockType.REPORTER, + text: "remove vector thorns from [SVG]", + arguments: { + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } + } + } ], menus: { - CHANNELS: { - acceptReporters: true, - items: ["R", "G", "B"] - }, + CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] }, POSITIONS: ["X", "Y"], + PIXELTYPE: ["total", "per line", "per row"], REMOVAL: ["under", "over", "equal to"], fileType: ["content", "dataURI"], EFFECTS: { @@ -322,23 +378,17 @@ }; } - setCutout(args) { - this.cutoutX = Scratch.Cast.toNumber(args.X); - this.cutoutY = Scratch.Cast.toNumber(args.Y); - } + setCutout(args) { this.cutPos = [Scratch.Cast.toNumber(args.X), Scratch.Cast.toNumber(args.Y)] } changeCutout(args) { - this.cutoutX = this.cutoutX + Scratch.Cast.toNumber(args.X); - this.cutoutY = this.cutoutY + Scratch.Cast.toNumber(args.Y); + this.cutPos = [this.cutPos[0] + Scratch.Cast.toNumber(args.X), + this.cutPos[1] + Scratch.Cast.toNumber(args.Y)]; } - currentCut(args) { return args.POS === "X" ? this.cutoutX : this.cutoutY } + currentCut(args) { return this.cutPos[args.POS === "X" ? 0 : 1] } - setScale(args) { - this.scale[0] = Scratch.Cast.toNumber(args.SIZE); - this.scale[1] = Scratch.Cast.toNumber(args.Y); - } + setScale(args) { this.scale = [Scratch.Cast.toNumber(args.SIZE), Scratch.Cast.toNumber(args.Y)] } changeScale(args) { - this.scale[0] = this.scale[0] + Scratch.Cast.toNumber(args.SIZE); - this.scale[1] = this.scale[1] + Scratch.Cast.toNumber(args.Y); + this.scale = [this.scale[0] + Scratch.Cast.toNumber(args.SIZE), + this.scale[1] + Scratch.Cast.toNumber(args.Y)]; } currentScale(args) { return this.scale[args.POS === "X" ? 0 : 1] } @@ -351,42 +401,61 @@ } currentDir() { return this.cutoutDirection } + clipImage(args) { + return new Promise((resolve, reject) => { + const mainImage = new Image(); + mainImage.onload = () => { + const cutoutImage = new Image(); + cutoutImage.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = mainImage.width; + canvas.height = mainImage.height; + const context = canvas.getContext("2d"); + const scaledWidth = cutoutImage.width + this.scale[0]; + const scaledHeight = cutoutImage.height + this.scale[1]; + const cutX = this.cutPos[0] + mainImage.width / 2 - scaledWidth / 2; + const cutY = this.cutPos[1] - mainImage.height / 2 + scaledHeight / 2; + + context.drawImage(mainImage, 0, 0); + context.globalCompositeOperation = "destination-in"; + const rotationAngle = ((this.cutoutDirection + 270) * Math.PI) / 180; + context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); + context.rotate(rotationAngle); + context.drawImage(cutoutImage, -scaledWidth / 2, + -scaledHeight / 2, scaledWidth, scaledHeight + ); + context.setTransform(1, 0, 0, 1, 0, 0); + context.globalCompositeOperation = "source-over"; + resolve(canvas.toDataURL("image/png")); + }; + cutoutImage.src = this.confirmAsset(args.CUTOUT, "png"); + }; + mainImage.src = this.confirmAsset(args.MAIN, "png"); + }); + } + setSoftness(args) { this.softness = Scratch.Cast.toNumber(args.AMT) } convertHexToRGB(args) { const hexColor = args.HEX; - if (args.CHANNEL === "R") { - return parseInt(hexColor.substring(1, 3), 16); - } else if (args.CHANNEL === "G") { - return parseInt(hexColor.substring(3, 5), 16); - } else { - return parseInt(hexColor.substring(5, 7), 16); - } + const channelOffset = { R: 1, G: 3, B: 5 }[args.CHANNEL]; + return parseInt(hexColor.substring(channelOffset, channelOffset + 2), 16); } applyHueEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; const color = hexToRgb(args.COLOR); const img = new Image(); img.onload = async () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - await this.applyHue(imageData, color[0], color[1], color[2]); - ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + const pixelData = this.printImg(img); + await this.applyHue(pixelData, color[0], color[1], color[2]); + resolve(this.exportImg(img, pixelData)); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - applyHue(imageData, r, g, b) { - const data = imageData.data; + applyHue(pixelData, r, g, b) { + const data = pixelData; for (let i = 0; i < data.length; i += 4) { data[i] = Math.min(255, (data[i] * r) / 255); data[i + 1] = Math.min(255, (data[i + 1] * g) / 255); @@ -396,54 +465,33 @@ deleteColor(args) { return this.replaceColor({ - COLOR : args.COLOR, - REPLACE : "#00000000", - DATA_URI : args.DATA_URI + COLOR : args.COLOR, REPLACE : "#00000000", DATA_URI : args.DATA_URI }); } replaceColor(args) { const colRem = hexToRgb(args.COLOR); const colRep = hexToRgb(args.REPLACE); - const dataURI = args.DATA_URI; - const canvasElement = document.createElement("canvas"); - const context = canvasElement.getContext("2d"); - const imageElement = new Image(); - const softness = this.softness; return new Promise(resolve => { + const imageElement = new Image(); imageElement.onload = () => { - canvasElement.width = imageElement.width; - canvasElement.height = imageElement.height; - context.drawImage(imageElement, 0, 0); - const imageData = context.getImageData(0, 0, canvasElement.width, canvasElement.height); - const data = imageData.data; - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - if ( - r >= colRem[0] - softness && r <= colRem[0] + softness && - g >= colRem[1] - softness && g <= colRem[1] + softness && - b >= colRem[2] - softness && b <= colRem[2] + softness - ) { - data[i] = colRep[0]; - data[i + 1] = colRep[1]; - data[i + 2] = colRep[2]; - data[i + 3] = colRep[3]; + const pixelData = this.printImg(imageElement); + for (let i = 0; i < pixelData.length; i += 4) { + const [r, g, b] = pixelData.slice(i, i + 3); + const inRange = (val, target) => val >= target - this.softness && val <= target + this.softness; + if (inRange(r, colRem[0]) && inRange(g, colRem[1]) && inRange(b, colRem[2])) { + pixelData.set(colRep, i); } } - context.putImageData(imageData, 0, 0); - const newDataURI = canvasElement.toDataURL("image/png"); - resolve(newDataURI); + resolve(this.exportImg(imageElement, pixelData)); }; - imageElement.src = dataURI; + imageElement.src = this.confirmAsset(args.DATA_URI, "png"); }); } applyEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; - const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) || 100; + const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) + 1 || 101; // let 0 pass const img = new Image(); img.onload = async () => { const canvas = document.createElement("canvas"); @@ -455,82 +503,33 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const effectFunction = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; if (effectFunction && typeof effectFunction === "function") { - await effectFunction(imageData, percentage); + await effectFunction(imageData, percentage - 1); } else { - this.applySaturation(imageData, percentage); + this.applySaturation(imageData, percentage - 1); } ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } applySaturation(imageData, percentage) { const data = imageData.data; + const percent = Scratch.Cast.toNumber(percentage) / 100; for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const avg = (r + g + b) / 3; - data[i] = avg + (r - avg) * (percentage / 100); - data[i + 1] = avg + (g - avg) * (percentage / 100); - data[i + 2] = avg + (b - avg) * (percentage / 100); + const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; + for (let j = 0; j < 3; j++) { data[i + j] = avg + (data[i + j] - avg) * percent } } } - clipImage(args) { - return new Promise((resolve, reject) => { - const mainImage = new Image(); - mainImage.onload = () => { - const cutoutImage = new Image(); - cutoutImage.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = mainImage.width; - canvas.height = mainImage.height; - const context = canvas.getContext("2d"); - const scaledWidth = cutoutImage.width + this.scale[0]; - const scaledHeight = cutoutImage.height + this.scale[1]; - const cutX = this.cutoutX + mainImage.width / 2 - scaledWidth / 2; - const cutY = this.cutoutY - mainImage.height / 2 + scaledHeight / 2; - - context.drawImage(mainImage, 0, 0); - context.globalCompositeOperation = "destination-in"; - const rotationAngle = ((this.cutoutDirection + 270) * Math.PI) / 180; - context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); - context.rotate(rotationAngle); - context.drawImage( - cutoutImage, - -scaledWidth / 2, - -scaledHeight / 2, - scaledWidth, - scaledHeight - ); - context.setTransform(1, 0, 0, 1, 0, 0); - context.globalCompositeOperation = "source-over"; - const clippedDataURI = canvas.toDataURL("image/png"); - resolve(clippedDataURI); - }; - cutoutImage.src = args.CUTOUT; - }; - mainImage.src = args.MAIN; - }); - } - applyGlitch(imageData, percentage) { const data = imageData.data; const percent = Scratch.Cast.toNumber(percentage); for (let i = 0; i < data.length; i += 4) { - const randomChance = Math.random() * 100; - if (randomChance <= percentage) { - const negative = Math.random() < 0.5 ? -1 : 1; - const rndR = Math.random() * (percentage * 1.5 * negative); - const rndG = Math.random() * (percentage * 1.5 * negative); - const rndB = Math.random() * (percentage * 1.5 * negative); - data[i] = (data[i] + rndR) % 256; - data[i + 1] = (data[i + 1] + rndG) % 256; - data[i + 2] = (data[i + 2] + rndB) % 256; + if (Math.random() * 100 <= percentage) { + const rnd = () => (Math.random() - 0.5) * percent * 3; + for (let j = 0; j < 3; j++) { data[i + j] = (data[i + j] + rnd()) % 256 } } } } @@ -539,25 +538,19 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const centerX = width / 2; - const centerY = height / 2; - const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY); + let center = [width / 2, height / 2]; + const maxDistance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); const percent = Scratch.Cast.toNumber(percentage); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; - const distanceX = Math.abs(x - centerX); - const distanceY = Math.abs(y - centerY); - const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); - let vigAMT = ""; - if (percent < 0) { - vigAMT = 1 - (distance / maxDistance) * (percent / 100); - } else { - vigAMT = ((maxDistance - distance) / maxDistance) * (percent / 100); + center = [Math.abs(x - center[0]), Math.abs(y - center[1])]; + const distance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); + let vigAMT = (percent < 0) ? 1 - (distance / maxDistance) * (percent / 100) : ((maxDistance - distance) / maxDistance) * (percent / 100); + vigAMT = Math.max(0, Math.min(1, vigAMT)); + for (let i = 0; i < 3; i++) { + data[index + i] = Math.round(data[index + i] * vigAMT); } - data[index] = Math.max(0, Math.min(255, data[index] * vigAMT)); - data[index + 1] = Math.max(0, Math.min(255, data[index + 1] * vigAMT)); - data[index + 2] = Math.max(0, Math.min(255, data[index + 2] * vigAMT)); } } } @@ -577,16 +570,10 @@ const offset = Math.sin(distance * (percentage / 100)) * (percentage / 100); const sourceX = Math.floor(x + offset); const sourceY = Math.floor(y); - if ( - sourceX >= 0 && sourceX < width && - sourceY >= 0 && sourceY < height - ) { + if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { const sourceIndex = (sourceY * width + sourceX) * 4; if (data[sourceIndex + 3] > 0) { - data[index] = data[sourceIndex]; - data[index + 1] = data[sourceIndex + 1]; - data[index + 2] = data[sourceIndex + 2]; - data[index + 3] = data[sourceIndex + 3]; + data.copyWithin(index, sourceIndex, sourceIndex + 4); } else { data[index + 3] = 0; } @@ -604,17 +591,12 @@ const newData = new Uint8ClampedArray(data.length); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const srcX = x + - Math.floor(Math.random() * dispAmt * 2 - dispAmt); - const srcY = y + - Math.floor(Math.random() * dispAmt * 2 - dispAmt); + const srcX = x + Math.floor(Math.random() * dispAmt * 2 - dispAmt); + const srcY = y + Math.floor(Math.random() * dispAmt * 2 - dispAmt); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; - newData[dstIndex] = data[srcIndex]; - newData[dstIndex + 1] = data[srcIndex + 1]; - newData[dstIndex + 2] = data[srcIndex + 2]; - newData[dstIndex + 3] = data[srcIndex + 3]; + newData.set(data.subarray(srcIndex, srcIndex + 4), dstIndex); } } } @@ -625,18 +607,14 @@ const data = imageData.data; const numLevels = Math.max(percentage / 10, 1); for (let i = 0; i < data.length; i += 4) { - data[i] = - Math.round((data[i] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); - data[i + 1] = - Math.round((data[i + 1] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); - data[i + 2] = - Math.round((data[i + 2] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); + for (let j = 0; j < 3; j++) { + data[i + j] = Math.round((data[i + j] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); + } } } applyBulgeEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; let centerX = Scratch.Cast.toNumber(args.CENTER_X) / 100 || 0; let centerY = Scratch.Cast.toNumber(args.CENTER_Y) / -100 || 0; const strength = Scratch.Cast.toNumber(args.STRENGTH) / 100 || 0; @@ -652,13 +630,11 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyBulge(imageData, centerX, centerY, strength); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - applyBulge(imageData, centerX, centerY, strength) { const data = imageData.data; const width = imageData.width; @@ -675,10 +651,7 @@ if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; - newData[dstIndex] = data[srcIndex]; - newData[dstIndex + 1] = data[srcIndex + 1]; - newData[dstIndex + 2] = data[srcIndex + 2]; - newData[dstIndex + 3] = data[srcIndex + 3]; + newData.set(data.subarray(srcIndex, srcIndex + 4), dstIndex); } } } @@ -687,7 +660,6 @@ applyWaveEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; const amplitudeX = Scratch.Cast.toNumber(args.AMPX) / 10 || 0; const amplitudeY = Scratch.Cast.toNumber(args.AMPY) / 10 || 0; const frequencyX = Scratch.Cast.toNumber(args.FREQX) / 100 || 0; @@ -702,13 +674,11 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY) { const data = imageData.data; const width = imageData.width; @@ -721,10 +691,7 @@ if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; - newData[dstIndex] = data[srcIndex]; - newData[dstIndex + 1] = data[srcIndex + 1]; - newData[dstIndex + 2] = data[srcIndex + 2]; - newData[dstIndex + 3] = data[srcIndex + 3]; + newData.set(data.subarray(srcIndex, srcIndex + 4), dstIndex); } } } @@ -735,112 +702,24 @@ const data = imageData.data; const width = imageData.width; const height = imageData.height; - const radius = percentage > 1 ? Math.floor((percentage / 100) * 10) : 0; + const radius = Math.floor((percentage / 100) * 10); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - let r = 0, - g = 0, - b = 0, - a = 0, - count = 0; + let sum = [0, 0, 0, 0]; + let count = 0; for (let ky = -radius; ky <= radius; ky++) { for (let kx = -radius; kx <= radius; kx++) { const offsetX = x + kx; const offsetY = y + ky; - if ( - offsetX >= 0 && offsetX < width && - offsetY >= 0 && offsetY < height - ) { + if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { const pixelIndex = (offsetY * width + offsetX) * 4; - r += data[pixelIndex]; - g += data[pixelIndex + 1]; - b += data[pixelIndex + 2]; - a += data[pixelIndex + 3]; + for (let i = 0; i < 4; i++) { sum[i] += data[pixelIndex + i] } count++; } } } const pixelIndex = (y * width + x) * 4; - if (a === 0) { - data[pixelIndex + 3] = a / count; - } else { - data[pixelIndex] = r / count; - data[pixelIndex + 1] = g / count; - data[pixelIndex + 2] = b / count; - data[pixelIndex + 3] = a / count; - } - } - } - } - - applyLineGlitchEffect(args) { - return new Promise((resolve) => { - const svgDataUri = args.SVG; - const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) / 100 || 0; - const direction = args.DIRECT; - const width = Scratch.Cast.toNumber(args.WIDTH) / 50 || 0; - 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); - let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyLineGlitch(imageData, percentage, direction, width); - ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); - }; - img.src = svgDataUri; - }); - } - - applyLineGlitch(imageData, percentage, direction, width) { - const data = imageData.data; - const imgWidth = imageData.width; - const imgHeight = imageData.height; - const numLines = Math.floor(imgHeight * percentage); - for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { - const linePosition = Math.floor(Math.random() * imgHeight); - const lineStart = linePosition - Math.floor(width / 2); - const lineEnd = lineStart + width; - if (direction === "Y") { - for (let y = 0; y < imgHeight; y++) { - for (let x = lineStart; x < lineEnd; x++) { - const srcX = x; - const srcY = linePosition; - if ( - srcX >= 0 && srcX < imgWidth && - srcY >= 0 && srcY < imgHeight - ) { - const srcIndex = (srcY * imgWidth + srcX) * 4; - const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; - data[dstIndex + 1] = data[srcIndex + 1]; - data[dstIndex + 2] = data[srcIndex + 2]; - data[dstIndex + 3] = data[srcIndex + 3]; - } - } - } - } else { - for (let y = lineStart; y < lineEnd; y++) { - for (let x = 0; x < imgWidth; x++) { - const srcX = linePosition; - const srcY = y; - if ( - srcX >= 0 && srcX < imgWidth && - srcY >= 0 && srcY < imgHeight - ) { - const srcIndex = (srcY * imgWidth + srcX) * 4; - const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; - data[dstIndex + 1] = data[srcIndex + 1]; - data[dstIndex + 2] = data[srcIndex + 2]; - data[dstIndex + 3] = data[srcIndex + 3]; - } - } - } + if (count > 0) for (let i = 0; i < 4; i++) { data[pixelIndex + i] = sum[i] / count } } } } @@ -852,20 +731,15 @@ const imgHeight = imageData.height; const numLines = Math.floor(imgWidth * 1); for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { - const linePosition = Math.floor(Math.random() * imgHeight); - const lineStart = linePosition - Math.floor(newWidth / 2); + const linePos = Math.floor(Math.random() * imgHeight); + const lineStart = linePos - Math.floor(newWidth / 2); const lineEnd = lineStart + newWidth; for (let y = 0; y < imgHeight; y++) { - for (let x = lineStart; x < lineEnd; x++) { - const srcX = linePosition; - const srcY = y; - if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { - const srcIndex = (srcY * imgWidth + srcX) * 4; + const srcIndex = (y * imgWidth + linePos) * 4; + if (linePos >= 0 && linePos < imgWidth) { + for (let x = lineStart; x < lineEnd; x++) { const dstIndex = (y * imgWidth + x) * 4; - data[dstIndex] = data[srcIndex]; - data[dstIndex + 1] = data[srcIndex + 1]; - data[dstIndex + 2] = data[srcIndex + 2]; - data[dstIndex + 3] = data[srcIndex + 3]; + data.copyWithin(dstIndex, srcIndex, srcIndex + 4); } } } @@ -874,7 +748,6 @@ removeTransparencyEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; const threshold = Scratch.Cast.toNumber(args.THRESHOLD) / 100 || 0; const removeUnder = args.REMOVE; const img = new Image(); @@ -887,33 +760,70 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyTransparencyRemoval(imageData, threshold, removeUnder); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - applyTransparencyRemoval(imageData, threshold, removeUnder) { const data = imageData.data; const pixelCount = data.length / 4; for (let i = 0; i < pixelCount; i++) { const alpha = data[i * 4 + 3] / 255; - if ( - (removeUnder === "under" && alpha < threshold) || + if ((removeUnder === "under" && alpha < threshold) || (removeUnder === "over" && alpha > threshold) || - (removeUnder === "equal to" && - alpha > threshold - 0.01 && - alpha < threshold + 0.01) - ) { + (removeUnder === "equal to" && alpha > threshold - 0.01 && + alpha < threshold + 0.01)) { data[i * 4 + 3] = 0; } } } + applyLineGlitchEffect(args) { + return new Promise((resolve) => { + const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) / 100 || 0; + const direction = args.DIRECT; + const width = Scratch.Cast.toNumber(args.WIDTH) / 50 || 0; + 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); + let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + this.applyLineGlitch(imageData, percentage, direction, width); + ctx.putImageData(imageData, 0, 0); + resolve(canvas.toDataURL()); + }; + img.src = this.confirmAsset(args.SVG, "png"); + }); + } + applyLineGlitch(imageData, percentage, direction, width) { + const data = imageData.data; + const imgWidth = imageData.width; + const imgHeight = imageData.height; + const numLines = Math.floor(imgHeight * percentage); + for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { + const linePosition = Math.floor(Math.random() * imgHeight); + const lineStart = linePosition - Math.floor(width / 2); + const lineEnd = lineStart + width; + for (let y = (direction === "Y" ? 0 : lineStart); y < (direction === "Y" ? imgHeight : lineEnd); y++) { + for (let x = (direction === "Y" ? lineStart : 0); x < (direction === "Y" ? lineEnd : imgWidth); x++) { + const srcX = (direction === "Y" ? x : linePosition); + const srcY = (direction === "Y" ? linePosition : y); + if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { + const srcIndex = (srcY * imgWidth + srcX) * 4; + const dstIndex = (y * imgWidth + x) * 4; + data.copyWithin(dstIndex, srcIndex, srcIndex + 4); + } + } + } + } + } + applyEdgeOutlineEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; const thickness = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); const color = hexToRgb(args.COLOR); const a = Math.min(Math.max(args.A, 0), 100) * 2.55; @@ -927,13 +837,11 @@ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this.applyOutline(imageData, thickness, color[0], color[1], color[2], a); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - applyOutline(imageData, thickness, r, g, b, a) { const data = imageData.data; const width = imageData.width; @@ -942,8 +850,7 @@ for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; - const alpha = data[index + 3]; - if (alpha < 255) { + if (data[index + 3] < 255) { for (let dy = -thickness; dy <= thickness; dy++) { for (let dx = -thickness; dx <= thickness; dx++) { const nx = x + dx; @@ -952,10 +859,7 @@ const neighborIndex = (ny * width + nx) * 4; const neighborAlpha = copyData[neighborIndex + 3]; if (neighborAlpha === 255) { - data[index] = r; - data[index + 1] = g; - data[index + 2] = b; - data[index + 3] = a; + data.set([r, g, b, a], index); break; } } @@ -972,24 +876,18 @@ const height = imageData.height; const percent = percentage / 100; const numPixelsToEnlarge = Math.floor((percent / 100) * (width * height)); - const maxEnlargeFactor = 1.5 + percent / 200; for (let i = 0; i < numPixelsToEnlarge; i++) { const x = Math.floor(Math.random() * width); const y = Math.floor(Math.random() * height); const index = (y * width + x) * 4; - const enlargeFactor = 1 + Math.random() * maxEnlargeFactor; + const enlargeFactor = 1 + Math.random() * (1.5 + percent / 200); const blurRadius = Math.floor(enlargeFactor * 4); for (let offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) { for (let offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) { const newX = x + offsetX; const newY = y + offsetY; - if (newX >= 0 && newX < width && newY >= 0 && newY < height) { - const newIndex = (newY * width + newX) * 4; - data[newIndex] = data[index]; - data[newIndex + 1] = data[index + 1]; - data[newIndex + 2] = data[index + 2]; - data[newIndex + 3] = data[index + 3]; - } + const bound = newX >= 0 && newX < width && newY >= 0 && newY < height; + if (bound) data.copyWithin((newY * width + newX) * 4, index, index + 4); } } } @@ -1036,25 +934,23 @@ const percent = percentage === 0 || percentage === "" ? 1 : Math.abs(Scratch.Cast.toNumber(percentage)); for (let y = 0; y < height; y += percent) { for (let x = 0; x < width; x += percent) { - const startX = x; const endX = Math.min(x + percent, width); - const startY = y; const endY = Math.min(y + percent, height); const avgColor = [0, 0, 0]; - for (let j = startY; j < endY; j++) { - for (let i = startX; i < endX; i++) { + for (let j = y; j < endY; j++) { + for (let i = x; i < endX; i++) { const index = (j * width + i) * 4; avgColor[0] += data[index]; avgColor[1] += data[index + 1]; avgColor[2] += data[index + 2]; } } - const totalPixels = (endX - startX) * (endY - startY); + const totalPixels = (endX - x) * (endY - y); avgColor[0] /= totalPixels; avgColor[1] /= totalPixels; avgColor[2] /= totalPixels; - for (let j = startY; j < endY; j++) { - for (let i = startX; i < endX; i++) { + for (let j = y; j < endY; j++) { + for (let i = x; i < endX; i++) { const index = (j * width + i) * 4; data[index] = avgColor[0]; data[index + 1] = avgColor[1]; @@ -1067,7 +963,6 @@ applyAbberationEffect(args) { return new Promise((resolve) => { - const svgDataUri = args.SVG; const percentage = args.PERCENTAGE; const img = new Image(); img.onload = () => { @@ -1075,41 +970,31 @@ canvas.width = img.width + Math.abs(percentage) * 5; canvas.height = img.height + Math.abs(percentage) * 5; const ctx = canvas.getContext("2d"); - ctx.drawImage( - img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5 - ); + ctx.drawImage(img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyChromaticAberration( - imageData, args.COLOR1, args.COLOR2, + this.applyChromAb(imageData, args.COLOR1, args.COLOR2, percentage, args.DIRECT); ctx.putImageData(imageData, 0, 0); - const modifiedDataUrl = canvas.toDataURL(); - resolve(modifiedDataUrl); + resolve(canvas.toDataURL()); }; - img.src = svgDataUri; + img.src = this.confirmAsset(args.SVG, "png"); }); } - - applyChromaticAberration(imageData, color1, color2, percentage, direction) { + applyChromAb(imageData, color1, color2, percentage, direction) { const data = imageData.data; let width = imageData.width; let height = imageData.height; const copy1 = new Uint8ClampedArray(data.length); const copy2 = new Uint8ClampedArray(data.length); - const hexToRGB = (hex) => [ - parseInt(hex.slice(1, 3), 16), - parseInt(hex.slice(3, 5), 16), - parseInt(hex.slice(5, 7), 16), - ]; - const rgb1 = hexToRGB(color1); - const rgb2 = hexToRGB(color2); + const rgb1 = hexToRgb(color1); + const rgb2 = hexToRgb(color2); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const srcIndex = (y * width + x) * 4; const r = data[srcIndex]; const g = data[srcIndex + 1]; const b = data[srcIndex + 2]; - const a = data[srcIndex + 3] / 1; + const a = data[srcIndex + 3]; let newX1, newY1, newX2, newY2; if (direction === "X") { newX1 = x + Math.floor((width / 2) * (percentage / 100)); @@ -1133,28 +1018,15 @@ const newR2 = data[(newY2 * width + newX2) * 4]; const newG2 = data[(newY2 * width + newX2) * 4 + 1]; const newB2 = data[(newY2 * width + newX2) * 4 + 2]; - const leftColor = [ - (rgb1[0] * r) / 255, - (rgb1[1] * g) / 255, - (rgb1[2] * b) / 255, - ]; - const rightColor = [ - (rgb2[0] * r) / 255, - (rgb2[1] * g) / 255, - (rgb2[2] * b) / 255, - ]; + const leftColor = [(rgb1[0] * r) / 255, (rgb1[1] * g) / 255, (rgb1[2] * b) / 255]; + const rightColor = [(rgb2[0] * r) / 255, (rgb2[1] * g) / 255, (rgb2[2] * b) / 255]; const leftIndex = (newY1 * width + newX1) * 4; const rightIndex = (newY2 * width + newX2) * 4; - - copy1[leftIndex] = leftColor[0]; - copy1[leftIndex + 1] = leftColor[1]; - copy1[leftIndex + 2] = leftColor[2]; - copy1[leftIndex + 3] = a; - - copy2[rightIndex] = rightColor[0]; - copy2[rightIndex + 1] = rightColor[1]; - copy2[rightIndex + 2] = rightColor[2]; - copy2[rightIndex + 3] = a; + for (let i = 0; i < 4; i++) { + copy1[leftIndex + i] = leftColor[i]; + copy2[rightIndex + i] = rightColor[i]; + } + copy1[leftIndex + 3] = copy2[rightIndex + 3] = a; } } for (let i = 0; i < data.length; i++) { @@ -1162,62 +1034,43 @@ } } - svgToBitmap(args) { - const svgContent = args.SVG; - const width = Math.abs(Scratch.Cast.toNumber(args.WIDTH)); - const height = Math.abs(Scratch.Cast.toNumber(args.HEIGHT)); + stretch(src, w, h) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext("2d"); - if (args.WIDTH < 0) { - ctx.translate(width, 0); - ctx.scale(-1, 1); - } - if (args.HEIGHT < 0) { - ctx.translate(0, height); - ctx.scale(1, -1); - } - ctx.drawImage(img, 0, 0, width, height); - const imageData = ctx.getImageData(0, 0, width, height); - const newCanvas = document.createElement("canvas"); - newCanvas.width = width; - newCanvas.height = height; - const newCtx = newCanvas.getContext("2d"); - newCtx.putImageData(imageData, 0, 0); - const dataUri = newCanvas.toDataURL(); - resolve(dataUri); + resolve(this.exportImg(img, this.printImg(img, w, h), w, h)); }; - img.src = `data:image/svg+xml;base64,${btoa(svgContent)}`; + img.src = src; }); } + svgToBitmap(args) { + return this.stretch(this.confirmAsset(args.URI, "svg"), + Math.abs(Scratch.Cast.toNumber(args.WIDTH)), Math.abs(Scratch.Cast.toNumber(args.HEIGHT)) + ); + } + stretchImg(args) { + return this.stretch(this.confirmAsset(args.URI, "png"), + Math.abs(Scratch.Cast.toNumber(args.W)), Math.abs(Scratch.Cast.toNumber(args.H)) + ); + } convertImageToSVG(args) { return new Promise((resolve) => { const img = new Image(); - img.src = args.URI; + img.src = this.confirmAsset(args.URI, "png"); 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, img.width, img.height); - const svg = document.createElementNS( - "http://www.w3.org/2000/svg", - "svg" - ); + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("version", "1.1"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); svg.setAttribute("width", img.width.toFixed(5)); svg.setAttribute("height", img.height.toFixed(5)); - svg.setAttribute( - "viewBox", - `0,0,${img.width.toFixed(5)},${img.height.toFixed(5)}` - ); + svg.setAttribute("viewBox", `0,0,${img.width.toFixed(5)},${img.height.toFixed(5)}`); const mergedColors = new Map(); for (let y = 0; y < img.height; y++) { for (let x = 0; x < img.width; x++) { @@ -1228,30 +1081,16 @@ const rightColorData = ctx.getImageData(x + 1, y, 1, 1).data; const rightColor = `rgb(${rightColorData[0]}, ${rightColorData[1]}, ${rightColorData[2]})`; if (color === rightColor) { - const mergedPixel = mergedColors.get(color) || { - x1: x, - y1: y, - x2: x + 1, - y2: y, - }; + const mergedPixel = mergedColors.get(color) || {x1: x, y1: y, x2: x + 1, y2: y}; mergedPixel.x2++; mergedColors.set(color, mergedPixel); } else { mergedColors.forEach((mergedPixel, colorKey) => { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); + const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); rect.setAttribute("x", mergedPixel.x1.toFixed(5)); rect.setAttribute("y", mergedPixel.y1.toFixed(5)); - rect.setAttribute( - "width", - (mergedPixel.x2 - mergedPixel.x1 + 1).toFixed(5) - ); - rect.setAttribute( - "height", - (mergedPixel.y2 - mergedPixel.y1 + 1).toFixed(5) - ); + rect.setAttribute("width", (mergedPixel.x2 - mergedPixel.x1 + 1).toFixed(5)); + rect.setAttribute("height", (mergedPixel.y2 - mergedPixel.y1 + 1).toFixed(5)); rect.setAttribute("fill", colorKey); svg.appendChild(rect); }); @@ -1284,35 +1123,85 @@ resolve(args.TYPE === "dataURI" ? `data:image/svg+xml;base64,${btoa(svg)}` : svg); }; img.onerror = reject; - img.src = args.URI; + img.src = this.confirmAsset(args.URI, "png"); }); } else { return args.URI; } } + upscaleImage(args) { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => { + const pixelData = this.printImg(img); + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), img.width, img.height), 0, 0); + const percentage = args.NUM * 10 || 0; + const factor = percentage / 100; + const weights = [0, -factor, 0, -factor, 1 + 4 * factor, -factor, 0, -factor, 0]; + this.sharpen(ctx, img.width, img.height, weights, 25); + resolve(this.exportImg(img, ctx.getImageData(0, 0, img.width, img.height).data)); + }; + img.src = this.confirmAsset(args.URI, "png"); + }); + } + sharpen(ctx, width, height, weights, alphaThreshold) { + const imageData = ctx.getImageData(0, 0, width, height); + const data = imageData.data; + const side = Math.round(Math.sqrt(weights.length)); + const halfSide = Math.floor(side / 2); + const output = ctx.createImageData(width, height); + const outputData = output.data; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const pixelIndex = (y * width + x) * 4; + let r = 0, g = 0, b = 0; + for (let ky = 0; ky < side; ky++) { + for (let kx = 0; kx < side; kx++) { + const weight = weights[ky * side + kx]; + const neighborY = Math.min(height - 1, Math.max(0, y + ky - halfSide)); + const neighborX = Math.min(width - 1, Math.max(0, x + kx - halfSide)); + const neighborPixelIndex = (neighborY * width + neighborX) * 4; + r += data[neighborPixelIndex] * weight; + g += data[neighborPixelIndex + 1] * weight; + b += data[neighborPixelIndex + 2] * weight; + } + } + if (data[pixelIndex + 3] / 255 > alphaThreshold / 50) { + outputData[pixelIndex] = this.clamp(r, 0, 255); + outputData[pixelIndex + 1] = this.clamp(g, 0, 255); + outputData[pixelIndex + 2] = this.clamp(b, 0, 255); + outputData[pixelIndex + 3] = 255; + } else { + outputData[pixelIndex + 3] = 0; + } + } + } + ctx.putImageData(output, 0, 0); + } + clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } + audioToImage(args) { const audioURI = args.AUDIO_URI; const imageWidth = Math.abs(Scratch.Cast.toString(args.W)); - const imageHeight = Math.abs(Scratch.Cast.toString(args.H)); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = imageWidth; - canvas.height = imageHeight; + canvas.height = Math.abs(Scratch.Cast.toString(args.H)); for (let i = 0; i < audioURI.length; i++) { const charCode = audioURI.charCodeAt(i); - const red = (charCode * 2) % 256; - const green = (charCode * 3) % 256; - const blue = (charCode * 4) % 256; - ctx.fillStyle = `rgb(${red},${green},${blue})`; + ctx.fillStyle = `rgb(${(charCode * 2) % 256},${(charCode * 3) % 256},${(charCode * 4) % 256})`; ctx.fillRect(i % imageWidth, Math.floor(i / imageWidth), 1, 1); } - const dataURI = canvas.toDataURL("image/png"); - return dataURI; + return canvas.toDataURL("image/png"); } skewSVG(args) { - let svg = this.updateView(args.SVG, Math.abs(args.X) + Math.abs(args.Y)); + svg = this.updateView(args.SVG, Math.abs(args.X) + Math.abs(args.Y)); const widthMatch = /width="([^"]*)"/.exec(svg); const heightMatch = /height="([^"]*)"/.exec(svg); if (widthMatch && heightMatch) { @@ -1323,8 +1212,7 @@ svg = svg.replace(/(style="[^"]*transform:[^"]*)/, `$1 skew(${args.Y}deg, ${args.X}deg)`); } else { svg = svg.replace( - `width="${width}" height="${height}"`, - `width="${width}" height="${height}" style="transform-origin: center; transform: skew(${args.Y}deg, ${args.X}deg)"` + `width="${width}" height="${height}"`, `width="${width}" height="${height}" style="transform-origin: center; transform: skew(${args.Y}deg, ${args.X}deg)"` ); } const currentTransform = /transform="([^"]*)"/.exec(svg); @@ -1356,6 +1244,64 @@ } return svg; } + + removeThorns(args) { return args.SVG.replaceAll("linejoin=\"miter\"", "linejoin=\"round\"") } + + numPixels(args) { + const img = new Image(); + img.src = this.confirmAsset(args.URI, "png"); + return new Promise((resolve) => { + img.onload = () => { + const pixelData = this.printImg(img); + resolve(args.TYPE === "total" ? pixelData.length / 4 : args.TYPE === "per line" ? img.width : img.height); + }; + }); + } + + setPixel(args) { return this.setPixels(args) } + setPixels(args) { + const img = new Image(); + img.src = this.confirmAsset(args.URI, "png"); + return new Promise((resolve) => { + img.onload = () => { + const startNum = Scratch.Cast.toNumber(args.NUM); + const endNum = Scratch.Cast.toNumber(args.NUM2) || startNum; + const pixelData = this.printImg(img); + for (let num = startNum; num <= endNum && num <= pixelData.length / 4; num++) { + const rgb = hexToRgb(args.COLOR); + for (let i = 0; i < 4; i++) { pixelData[((num - 1) * 4) + i] = rgb[i] } + } + resolve(this.exportImg(img, pixelData)); + }; + }); + } + + printImg(img, forceWid, forceHei) { + const canvas = document.createElement("canvas"); + canvas.width = forceWid || img.width; + canvas.height = forceHei || img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + return ctx.getImageData(0, 0, canvas.width, canvas.height).data; + } + + exportImg(img, pixelData, forceWid, forceHei) { + const canvas = document.createElement("canvas"); + canvas.width = forceWid || img.width; + canvas.height = forceHei || img.height; + const ctx = canvas.getContext("2d"); + ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), canvas.width, canvas.height), 0, 0); + return canvas.toDataURL(); + } + + confirmAsset(input, type) { + if (!input || !input.startsWith("data:image/") && !input.startsWith(" Date: Thu, 14 Dec 2023 20:36:29 -0800 Subject: [PATCH 09/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index a0727b3d83..cac15a3224 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -1201,7 +1201,7 @@ } skewSVG(args) { - svg = this.updateView(args.SVG, Math.abs(args.X) + Math.abs(args.Y)); + let svg = this.updateView(args.SVG, Math.abs(args.X) + Math.abs(args.Y)); const widthMatch = /width="([^"]*)"/.exec(svg); const heightMatch = /height="([^"]*)"/.exec(svg); if (widthMatch && heightMatch) { @@ -1295,7 +1295,7 @@ } confirmAsset(input, type) { - if (!input || !input.startsWith("data:image/") && !input.startsWith(" Date: Tue, 2 Jan 2024 00:23:30 -0800 Subject: [PATCH 10/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index cac15a3224..24ecb5f947 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.0.0 +// Version V.2.0.1 (function (Scratch) { "use strict"; @@ -1044,7 +1044,7 @@ }); } svgToBitmap(args) { - return this.stretch(this.confirmAsset(args.URI, "svg"), + return this.stretch(this.confirmAsset(args.SVG, "png"), Math.abs(Scratch.Cast.toNumber(args.WIDTH)), Math.abs(Scratch.Cast.toNumber(args.HEIGHT)) ); } From b49fc63f6fcacc0d155d5de366c9e6616a04d322 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:23:40 -0800 Subject: [PATCH 11/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 55 ++++++++++++++++++--------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 24ecb5f947..2c9e90582e 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.0.1 +// Version V.2.1.0 (function (Scratch) { "use strict"; @@ -21,6 +21,10 @@ const a = hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 255; return [r, g, b, a]; } + function rgbaToHex(r, g, b, a) { + const alpha = a !== undefined ? Math.round(a).toString(16).padStart(2, "0") : ""; + return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}${alpha}`; + } class imgEffectsSP { constructor() { @@ -259,6 +263,15 @@ TYPE: { type: Scratch.ArgumentType.STRING, menu: "PIXELTYPE" } } }, + { + opcode: "getPixel", + blockType: Scratch.BlockType.REPORTER, + text: "get hex of pixel #[NUM] in [URI]", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } + } + }, { opcode: "setPixel", blockType: Scratch.BlockType.REPORTER, @@ -504,9 +517,7 @@ const effectFunction = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; if (effectFunction && typeof effectFunction === "function") { await effectFunction(imageData, percentage - 1); - } else { - this.applySaturation(imageData, percentage - 1); - } + } else { this.applySaturation(imageData, percentage - 1) } ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; @@ -572,14 +583,9 @@ const sourceY = Math.floor(y); if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { const sourceIndex = (sourceY * width + sourceX) * 4; - if (data[sourceIndex + 3] > 0) { - data.copyWithin(index, sourceIndex, sourceIndex + 4); - } else { - data[index + 3] = 0; - } - } else { - data[index + 3] = 0; - } + if (data[sourceIndex + 3] > 0) data.copyWithin(index, sourceIndex, sourceIndex + 4); + else data[index + 3] = 0; + } else { data[index + 3] = 0 } } } } @@ -1125,9 +1131,7 @@ img.onerror = reject; img.src = this.confirmAsset(args.URI, "png"); }); - } else { - return args.URI; - } + } else { return args.URI } } upscaleImage(args) { @@ -1176,9 +1180,7 @@ outputData[pixelIndex + 1] = this.clamp(g, 0, 255); outputData[pixelIndex + 2] = this.clamp(b, 0, 255); outputData[pixelIndex + 3] = 255; - } else { - outputData[pixelIndex + 3] = 0; - } + } else { outputData[pixelIndex + 3] = 0 } } } ctx.putImageData(output, 0, 0); @@ -1275,6 +1277,21 @@ }; }); } + getPixel(args) { + const img = new Image(); + img.src = this.confirmAsset(args.URI, "png"); + return new Promise((resolve) => { + img.onload = () => { + const targetPixel = Scratch.Cast.toNumber(args.NUM); + const pixelData = this.printImg(img); + if (targetPixel >= 1 && targetPixel <= pixelData.length / 4) { + const pixelIndex = (targetPixel - 1) * 4; + const rgba = pixelData.slice(pixelIndex, pixelIndex + 4); + resolve(rgbaToHex(rgba[0], rgba[1], rgba[2], rgba[3])); + } else { resolve("#00000000") } + }; + }); + } printImg(img, forceWid, forceHei) { const canvas = document.createElement("canvas"); @@ -1295,7 +1312,7 @@ } confirmAsset(input, type) { - if (!input || (!input.startsWith("data:image/") && !input.startsWith(" Date: Wed, 31 Jan 2024 18:04:20 -0800 Subject: [PATCH 12/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 72 ++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 2c9e90582e..b72b7898e4 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.1.0 +// Version V.2.2.0 (function (Scratch) { "use strict"; @@ -32,6 +32,7 @@ this.scale = [100, 100]; this.cutoutDirection = 90; this.softness = 10; + this.allShards = []; } getInfo() { return { @@ -178,6 +179,7 @@ CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } } }, + "---", { opcode: "setCutout", blockType: Scratch.BlockType.COMMAND, @@ -205,6 +207,7 @@ POS: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" } } }, + "---", { opcode: "setScale", blockType: Scratch.BlockType.COMMAND, @@ -232,6 +235,7 @@ POS: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" } } }, + "---", { opcode: "setDirection", blockType: Scratch.BlockType.COMMAND, @@ -253,6 +257,24 @@ blockType: Scratch.BlockType.REPORTER, text: "clipping direction" }, + "---", + { + opcode: "crackImage", + blockType: Scratch.BlockType.COMMAND, + text: "crack [URI] into [SHARDS] shards", + arguments: { + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SHARDS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } + } + }, + { + opcode: "getShard", + blockType: Scratch.BlockType.REPORTER, + text: "get shard #[SHARD]", + arguments: { + SHARD: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } + } + }, { blockType: Scratch.BlockType.LABEL, text: "Pixels" }, { opcode: "numPixels", @@ -1293,6 +1315,47 @@ }); } + crackImage(args) { + const cracks = Math.max(2, args.SHARDS); + const img = new Image(); + img.src = this.confirmAsset(args.URI, "png"); + const newWidth = img.width * 4; + const newHeight = img.height * 4; + this.allShards = []; + return new Promise((resolve) => { + img.onload = () => { + for (let i = 0; i < cracks; i++) { + if (this.allShards.length >= args.SHARDS) break; + for (let j = 0; j < cracks; j++) { + if (this.allShards.length >= args.SHARDS) break; + const shardCanvas = document.createElement("canvas"); + const shardWidth = newWidth / cracks; + const shardHeight = newHeight / cracks; + shardCanvas.width = shardWidth; + shardCanvas.height = shardHeight; + const ctx = shardCanvas.getContext("2d"); + ctx.clearRect(0, 0, shardWidth, shardHeight); + ctx.beginPath(); + ctx.moveTo(Math.random() * shardWidth, Math.random() * shardHeight); + for (let k = 0; k < Math.random() * 10 + 3; k++) { + ctx.lineTo(Math.random() * shardWidth, Math.random() * shardHeight); + } + ctx.closePath(); + ctx.clip(); + const offsetX = Math.random() * (newWidth - shardWidth); + const offsetY = Math.random() * (newHeight - shardHeight); + ctx.drawImage(img, -offsetX, -offsetY, newWidth, newHeight); + const pixelData = this.printImg(shardCanvas); + this.allShards.push(this.exportImg(shardCanvas, pixelData)); + } + } + resolve(); + }; + }); + } + + getShard(args) { return this.allShards[args.SHARD - 1] || "" } + printImg(img, forceWid, forceHei) { const canvas = document.createElement("canvas"); canvas.width = forceWid || img.width; @@ -1313,11 +1376,8 @@ confirmAsset(input, type) { if (!input || !(input.startsWith("data:image/") || input.startsWith(" Date: Sun, 3 Mar 2024 20:56:25 -0800 Subject: [PATCH 13/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 41 +++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index b72b7898e4..db4bbd9817 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,11 +3,10 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.2.0 +// Version V.2.3.1 (function (Scratch) { "use strict"; - if (!Scratch.extensions.unsandboxed) throw new Error("Image Effects must run unsandboxed"); Scratch.vm.extensionManager.loadExtensionURL("https://extensions.turbowarp.org/Lily/Skins.js"); @@ -179,6 +178,15 @@ CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } } }, + { + opcode: "overlayImage", + blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] onto [MAIN]", + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, + CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, "---", { opcode: "setCutout", @@ -469,6 +477,35 @@ }); } + overlayImage(args) { + return new Promise((resolve, reject) => { + const mainImage = new Image(); + mainImage.onload = () => { + const cutoutImage = new Image(); + cutoutImage.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = Math.max(mainImage.width, cutoutImage.width); + canvas.height = Math.max(mainImage.height, cutoutImage.height); + const context = canvas.getContext("2d"); + + context.drawImage(mainImage, 0, 0); + const scaledWidth = cutoutImage.width + this.scale[0]; + const scaledHeight = cutoutImage.height + this.scale[1]; + const cutX = this.cutPos[0] + mainImage.width / 2 - scaledWidth / 2; + const cutY = this.cutPos[1] - mainImage.height / 2 + scaledHeight / 2; + + context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); + context.rotate(((this.cutoutDirection + 270) * Math.PI) / 180); + context.drawImage(cutoutImage, -scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight); + context.setTransform(1, 0, 0, 1, 0, 0); + resolve(canvas.toDataURL("image/png")); + }; + cutoutImage.src = this.confirmAsset(args.CUTOUT, "png"); + }; + mainImage.src = this.confirmAsset(args.MAIN, "png"); + }); + } + setSoftness(args) { this.softness = Scratch.Cast.toNumber(args.AMT) } convertHexToRGB(args) { From b3098da8a0c10f024b4f6a50de1cce0f3f428116 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sun, 3 Mar 2024 20:58:44 -0800 Subject: [PATCH 14/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index db4bbd9817..529e9c349a 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -1076,13 +1076,6 @@ newY1 = Math.max(0, Math.min(height - 1, newY1)); newX2 = Math.max(0, Math.min(width - 1, newX2)); newY2 = Math.max(0, Math.min(height - 1, newY2)); - const newR1 = data[(newY1 * width + newX1) * 4]; - const newG1 = data[(newY1 * width + newX1) * 4 + 1]; - const newB1 = data[(newY1 * width + newX1) * 4 + 2]; - - const newR2 = data[(newY2 * width + newX2) * 4]; - const newG2 = data[(newY2 * width + newX2) * 4 + 1]; - const newB2 = data[(newY2 * width + newX2) * 4 + 2]; const leftColor = [(rgb1[0] * r) / 255, (rgb1[1] * g) / 255, (rgb1[2] * b) / 255]; const rightColor = [(rgb2[0] * r) / 255, (rgb2[1] * g) / 255, (rgb2[2] * b) / 255]; const leftIndex = (newY1 * width + newX1) * 4; From 3b7ff6f4fdabd5378e63d2a0363d271ef4c373c2 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:41:24 -0700 Subject: [PATCH 15/26] Update Image-Effects.js dont load skins --- extensions/SharkPool/Image-Effects.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 529e9c349a..55bf6d2a38 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,12 +3,11 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.3.1 +// Version V.2.3.2 (function (Scratch) { "use strict"; if (!Scratch.extensions.unsandboxed) throw new Error("Image Effects must run unsandboxed"); - Scratch.vm.extensionManager.loadExtensionURL("https://extensions.turbowarp.org/Lily/Skins.js"); const menuIconURI = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; From 7d4e193f7918635f44f555933b5018622886b036 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:13:51 -0700 Subject: [PATCH 16/26] Create Image-Effects.svg --- images/SharkPool/Image-Effects.svg | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 images/SharkPool/Image-Effects.svg diff --git a/images/SharkPool/Image-Effects.svg b/images/SharkPool/Image-Effects.svg new file mode 100644 index 0000000000..81973f04c6 --- /dev/null +++ b/images/SharkPool/Image-Effects.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5967166f6f831b132f006e1f4251f1abff0ee65b Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 14 May 2024 21:44:51 -0700 Subject: [PATCH 17/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 55bf6d2a38..6c7f482817 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.3.2 +// Version V.2.3.5 (function (Scratch) { "use strict"; @@ -308,7 +308,7 @@ arguments: { URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + COLOR: { type: Scratch.ArgumentType.COLOR } } }, { @@ -319,7 +319,7 @@ URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, NUM2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + COLOR: { type: Scratch.ArgumentType.COLOR } } }, { blockType: Scratch.BlockType.LABEL, text: "Image Conversions" }, @@ -411,7 +411,7 @@ EFFECTS: { acceptReporters: true, items: [ - "Saturation", "Glitch", "Chunk Glitch", "Clip Glitch", + "Saturation", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", "Vignette", "Ripple", "Displacement", "Posterize", "Blur", "Scanlines", "Grain", "Cubism", ] @@ -592,6 +592,14 @@ } } + applyOpaque(imageData, percentage) { + const data = imageData.data; + const percent = Math.max(Scratch.Cast.toNumber(percentage) / 100, 0); + for (let i = 0; i < data.length; i += 4) { + data[i + 3] = data[i + 3] * percent; + } + } + applyGlitch(imageData, percentage) { const data = imageData.data; const percent = Scratch.Cast.toNumber(percentage); From 76c445135eea20a2eafafdc7127e34b49efa1f0a Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 14 May 2024 21:49:36 -0700 Subject: [PATCH 18/26] Update Image-Effects.js --- extensions/SharkPool/Image-Effects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 6c7f482817..18f43780f1 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -594,7 +594,7 @@ applyOpaque(imageData, percentage) { const data = imageData.data; - const percent = Math.max(Scratch.Cast.toNumber(percentage) / 100, 0); + const percent = Math.max((Scratch.Cast.toNumber(percentage) + 100) / 100, 0); for (let i = 0; i < data.length; i += 4) { data[i + 3] = data[i + 3] * percent; } From 746c1fcafd74c73b36d498a9812c1aab907ef094 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:11:24 -0700 Subject: [PATCH 19/26] Image-Effects -- Remove Paper Data --- extensions/SharkPool/Image-Effects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 18f43780f1..e154f4fd82 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.3.5 +// Version V.2.3.51 (function (Scratch) { "use strict"; @@ -1180,7 +1180,7 @@ const height = img.height; const svg = ` - Date: Thu, 18 Jul 2024 17:29:58 -0700 Subject: [PATCH 20/26] Image-Effects -- Rewrite --- extensions/SharkPool/Image-Effects.js | 932 +++++++++++--------------- 1 file changed, 398 insertions(+), 534 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index e154f4fd82..d9c521a300 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,7 +3,7 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.3.51 +// Version V.2.4.0 (function (Scratch) { "use strict"; @@ -13,16 +13,20 @@ "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; function hexToRgb(hex) { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - const a = hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 255; - return [r, g, b, a]; + // returns [r,g,b,a] + return [ + parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16), + hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 255 + ]; } function rgbaToHex(r, g, b, a) { const alpha = a !== undefined ? Math.round(a).toString(16).padStart(2, "0") : ""; return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}${alpha}`; } + const imageEffectMenu = [ + "Saturation", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", "Vignette", + "Ripple", "Displacement", "Posterize", "Blur", "Scanlines", "Grain", "Cubism", + ]; class imgEffectsSP { constructor() { @@ -40,24 +44,13 @@ color1: "#9966FF", color2: "#774DCB", blocks: [ - { blockType: Scratch.BlockType.LABEL, text: "Effects" }, - { - opcode: "convertHexToRGB", - blockType: Scratch.BlockType.REPORTER, - text: "convert [HEX] to [CHANNEL]", - hideFromPalette: true, // depreciated block - arguments: { - HEX: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, - CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } - } - }, { opcode: "applyHueEffect", blockType: Scratch.BlockType.REPORTER, text: "apply hue [COLOR] to URI [SVG]", arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } + COLOR: { type: Scratch.ArgumentType.COLOR } } }, "---", @@ -66,7 +59,7 @@ blockType: Scratch.BlockType.REPORTER, text: "remove color [COLOR] from [DATA_URI]", arguments: { - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + COLOR: { type: Scratch.ArgumentType.COLOR }, DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } }, @@ -75,7 +68,7 @@ blockType: Scratch.BlockType.REPORTER, text: "replace color [COLOR] with [REPLACE] from [DATA_URI]", arguments: { - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, + COLOR: { type: Scratch.ArgumentType.COLOR }, REPLACE: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00ff00" }, DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } @@ -83,7 +76,7 @@ { opcode: "setSoftness", blockType: Scratch.BlockType.COMMAND, - text: "set softness of color detection to [AMT]%", + text: "set color detection softness to [AMT]%", arguments: { AMT: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 } } @@ -159,7 +152,7 @@ { opcode: "applyEdgeOutlineEffect", blockType: Scratch.BlockType.REPORTER, - text: "add outline to URI [SVG] with thickness [THICKNESS] and color [COLOR] opacity [A]%", + text: "add outline to URI [SVG] thickness [THICKNESS] color [COLOR] opacity [A]%", arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, THICKNESS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, @@ -169,21 +162,12 @@ }, { blockType: Scratch.BlockType.LABEL, text: "Clipping" }, { - opcode: "clipImage", - blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] from [MAIN]", - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, - CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "overlayImage", - blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] onto [MAIN]", + opcode: "maskImage", blockType: Scratch.BlockType.REPORTER, + text: "[TYPE] [MASK] from [IMG]", arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, - CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + TYPE: { type: Scratch.ArgumentType.STRING, menu: "MASKING" }, + IMG: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, + MASK: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } } }, "---", @@ -345,7 +329,7 @@ { opcode: "makeSVGimage", blockType: Scratch.BlockType.REPORTER, - text: "make new svg with image URI [URI] to svg [TYPE]", + text: "make svg with image URI [URI] to svg [TYPE]", arguments: { URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } @@ -400,144 +384,111 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } + }, + // Deprecated + { + opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, + text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, + arguments: { + HEX: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } + } } ], menus: { - CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] }, POSITIONS: ["X", "Y"], + MASKING: ["clip", "mask", "overlay"], PIXELTYPE: ["total", "per line", "per row"], REMOVAL: ["under", "over", "equal to"], fileType: ["content", "dataURI"], - EFFECTS: { - acceptReporters: true, - items: [ - "Saturation", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", - "Vignette", "Ripple", "Displacement", "Posterize", - "Blur", "Scanlines", "Grain", "Cubism", - ] - }, + EFFECTS: { acceptReporters: true, items: imageEffectMenu }, + CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated }, }; } - setCutout(args) { this.cutPos = [Scratch.Cast.toNumber(args.X), Scratch.Cast.toNumber(args.Y)] } - changeCutout(args) { - this.cutPos = [this.cutPos[0] + Scratch.Cast.toNumber(args.X), - this.cutPos[1] + Scratch.Cast.toNumber(args.Y)]; - } - currentCut(args) { return this.cutPos[args.POS === "X" ? 0 : 1] } + // Helper Funcs + clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } - setScale(args) { this.scale = [Scratch.Cast.toNumber(args.SIZE), Scratch.Cast.toNumber(args.Y)] } - changeScale(args) { - this.scale = [this.scale[0] + Scratch.Cast.toNumber(args.SIZE), - this.scale[1] + Scratch.Cast.toNumber(args.Y)]; + printImg(img, forceWid, forceHei) { + const { canvas, ctx } = this.createCanvasCtx(forceWid || img.width, forceHei || img.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + return ctx.getImageData(0, 0, canvas.width, canvas.height).data; } - currentScale(args) { return this.scale[args.POS === "X" ? 0 : 1] } - setDirection(args) { this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE) } - changeDirection(args) { - let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); - if (direction > 180) { direction = -180 + Scratch.Cast.toNumber(args.ANGLE) } - if (direction < -180) { direction = 180 + Scratch.Cast.toNumber(args.ANGLE) } - this.cutoutDirection = direction; + exportImg(img, pixelData, forceWid, forceHei) { + const { canvas, ctx } = this.createCanvasCtx(forceWid || img.width, forceHei || img.height); + ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), canvas.width, canvas.height), 0, 0); + return canvas.toDataURL(); } - currentDir() { return this.cutoutDirection } - - clipImage(args) { - return new Promise((resolve, reject) => { - const mainImage = new Image(); - mainImage.onload = () => { - const cutoutImage = new Image(); - cutoutImage.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = mainImage.width; - canvas.height = mainImage.height; - const context = canvas.getContext("2d"); - const scaledWidth = cutoutImage.width + this.scale[0]; - const scaledHeight = cutoutImage.height + this.scale[1]; - const cutX = this.cutPos[0] + mainImage.width / 2 - scaledWidth / 2; - const cutY = this.cutPos[1] - mainImage.height / 2 + scaledHeight / 2; - context.drawImage(mainImage, 0, 0); - context.globalCompositeOperation = "destination-in"; - const rotationAngle = ((this.cutoutDirection + 270) * Math.PI) / 180; - context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); - context.rotate(rotationAngle); - context.drawImage(cutoutImage, -scaledWidth / 2, - -scaledHeight / 2, scaledWidth, scaledHeight - ); - context.setTransform(1, 0, 0, 1, 0, 0); - context.globalCompositeOperation = "source-over"; - resolve(canvas.toDataURL("image/png")); - }; - cutoutImage.src = this.confirmAsset(args.CUTOUT, "png"); - }; - mainImage.src = this.confirmAsset(args.MAIN, "png"); - }); + createCanvasCtx(w, h, img, x, y) { + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (img !== undefined) ctx.drawImage(img, x, y); + return { canvas, ctx }; } - overlayImage(args) { - return new Promise((resolve, reject) => { - const mainImage = new Image(); - mainImage.onload = () => { - const cutoutImage = new Image(); - cutoutImage.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = Math.max(mainImage.width, cutoutImage.width); - canvas.height = Math.max(mainImage.height, cutoutImage.height); - const context = canvas.getContext("2d"); - - context.drawImage(mainImage, 0, 0); - const scaledWidth = cutoutImage.width + this.scale[0]; - const scaledHeight = cutoutImage.height + this.scale[1]; - const cutX = this.cutPos[0] + mainImage.width / 2 - scaledWidth / 2; - const cutY = this.cutPos[1] - mainImage.height / 2 + scaledHeight / 2; + getImageBounds(imageData) { + const { data, width, height } = imageData; + let minX = width, minY = height, maxX = 0, maxY = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (data[((y * width + x) * 4) + 3] > 0) { + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + } + return { width: maxX - minX + 1, height: maxY - minY + 1, offsetX: minX, offsetY: minY }; + } - context.translate(cutX + scaledWidth / 2, cutY * -1 + scaledHeight / 2); - context.rotate(((this.cutoutDirection + 270) * Math.PI) / 180); - context.drawImage(cutoutImage, -scaledWidth / 2, -scaledHeight / 2, scaledWidth, scaledHeight); - context.setTransform(1, 0, 0, 1, 0, 0); - resolve(canvas.toDataURL("image/png")); - }; - cutoutImage.src = this.confirmAsset(args.CUTOUT, "png"); - }; - mainImage.src = this.confirmAsset(args.MAIN, "png"); - }); + confirmAsset(input, type) { + if (!input || !(input.startsWith("data:image/") || input.startsWith(" { const color = hexToRgb(args.COLOR); const img = new Image(); img.onload = async () => { const pixelData = this.printImg(img); - await this.applyHue(pixelData, color[0], color[1], color[2]); + const data = pixelData; + for (let i = 0; i < data.length; i += 4) { + data[i] = Math.min(255, (data[i] * color[0]) / 255); + data[i + 1] = Math.min(255, (data[i + 1] * color[1]) / 255); + data[i + 2] = Math.min(255, (data[i + 2] * color[2]) / 255); + } resolve(this.exportImg(img, pixelData)); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyHue(pixelData, r, g, b) { - const data = pixelData; - for (let i = 0; i < data.length; i += 4) { - data[i] = Math.min(255, (data[i] * r) / 255); - data[i + 1] = Math.min(255, (data[i + 1] * g) / 255); - data[i + 2] = Math.min(255, (data[i + 2] * b) / 255); - } - } deleteColor(args) { - return this.replaceColor({ - COLOR : args.COLOR, REPLACE : "#00000000", DATA_URI : args.DATA_URI - }); + return this.replaceColor({ COLOR : args.COLOR, REPLACE : "#00000000", DATA_URI : args.DATA_URI }); } replaceColor(args) { @@ -550,9 +501,7 @@ for (let i = 0; i < pixelData.length; i += 4) { const [r, g, b] = pixelData.slice(i, i + 3); const inRange = (val, target) => val >= target - this.softness && val <= target + this.softness; - if (inRange(r, colRem[0]) && inRange(g, colRem[1]) && inRange(b, colRem[2])) { - pixelData.set(colRep, i); - } + if (inRange(r, colRem[0]) && inRange(g, colRem[1]) && inRange(b, colRem[2])) pixelData.set(colRep, i); } resolve(this.exportImg(imageElement, pixelData)); }; @@ -562,89 +511,104 @@ applyEffect(args) { return new Promise((resolve) => { - const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) + 1 || 101; // let 0 pass + const percent = Scratch.Cast.toNumber(args.PERCENTAGE); const img = new Image(); img.onload = async () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const effectFunction = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; - if (effectFunction && typeof effectFunction === "function") { - await effectFunction(imageData, percentage - 1); - } else { this.applySaturation(imageData, percentage - 1) } + const effectFunc = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; + if (effectFunc && typeof effectFunc === "function") await effectFunc(imageData, percent); + else resolve(""); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - - applySaturation(imageData, percentage) { + applySaturation(imageData, amtIn) { const data = imageData.data; - const percent = Scratch.Cast.toNumber(percentage) / 100; + amtIn /= 100; for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; - for (let j = 0; j < 3; j++) { data[i + j] = avg + (data[i + j] - avg) * percent } + for (let j = 0; j < 3; j++) { data[i + j] = avg + (data[i + j] - avg) * amtIn } } } - - applyOpaque(imageData, percentage) { + applyOpaque(imageData, amtIn) { const data = imageData.data; - const percent = Math.max((Scratch.Cast.toNumber(percentage) + 100) / 100, 0); - for (let i = 0; i < data.length; i += 4) { - data[i + 3] = data[i + 3] * percent; - } + amtIn = Math.max((amtIn + 100) / 100, 0); + for (let i = 0; i < data.length; i += 4) { data[i + 3] = data[i + 3] * amtIn } } - - applyGlitch(imageData, percentage) { + applyGlitch(imageData, amtIn) { const data = imageData.data; - const percent = Scratch.Cast.toNumber(percentage); for (let i = 0; i < data.length; i += 4) { - if (Math.random() * 100 <= percentage) { - const rnd = () => (Math.random() - 0.5) * percent * 3; + if (Math.random() * 100 <= amtIn) { + const rnd = () => (Math.random() - 0.5) * amtIn * 3; for (let j = 0; j < 3; j++) { data[i + j] = (data[i + j] + rnd()) % 256 } } } } - - applyVignette(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; + applyChunkGlitch(imageData, amtIn) { + const { data, width, height} = imageData; + const newWidth = amtIn / 10; + const numLines = Math.floor(width * 1); + for (let i = 0; i < Math.floor(width * 1); i++) { + const linePos = Math.floor(Math.random() * height); + const lineStart = linePos - Math.floor(newWidth / 2); + const lineEnd = lineStart + newWidth; + for (let y = 0; y < height; y++) { + const srcIndex = (y * width + linePos) * 4; + if (linePos >= 0 && linePos < width) { + for (let x = lineStart; x < lineEnd; x++) { + data.copyWithin((y * width + x) * 4, srcIndex, srcIndex + 4); + } + } + } + } + } + applyClipGlitch(imageData, amtIn) { + const { data, width, height} = imageData; + amtIn /= 100; + const numPixelsToEnlarge = Math.floor((amtIn / 100) * (width * height)); + for (let i = 0; i < numPixelsToEnlarge; i++) { + const x = Math.floor(Math.random() * width); + const y = Math.floor(Math.random() * height); + const index = (y * width + x) * 4; + const enlargeFactor = 1 + Math.random() * (1.5 + amtIn / 200); + const blurRadius = Math.floor(enlargeFactor * 4); + for (let offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) { + for (let offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) { + const newX = x + offsetX; + const newY = y + offsetY; + const bounded = newX >= 0 && newX < width && newY >= 0 && newY < height; + if (bounded) data.copyWithin((newY * width + newX) * 4, index, index + 4); + } + } + } + } + applyVignette(imageData, amtIn) { + const { data, width, height} = imageData; let center = [width / 2, height / 2]; const maxDistance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); - const percent = Scratch.Cast.toNumber(percentage); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; center = [Math.abs(x - center[0]), Math.abs(y - center[1])]; const distance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); - let vigAMT = (percent < 0) ? 1 - (distance / maxDistance) * (percent / 100) : ((maxDistance - distance) / maxDistance) * (percent / 100); + let vigAMT = (amtIn < 0) ? 1 - (distance / maxDistance) * (amtIn / 100) : ((maxDistance - distance) / maxDistance) * (amtIn / 100); vigAMT = Math.max(0, Math.min(1, vigAMT)); - for (let i = 0; i < 3; i++) { - data[index + i] = Math.round(data[index + i] * vigAMT); - } + for (let i = 0; i < 3; i++) { data[index + i] = Math.round(data[index + i] * vigAMT) } } } } - - applyRipple(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - const centerX = width / 2; - const centerY = height / 2; + applyRipple(imageData, amtIn) { + const { data, width, height} = imageData; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; - const dx = x - centerX; - const dy = y - centerY; + const dx = x - (width / 2); + const dy = y - (height / 2); const distance = Math.sqrt(dx * dx + dy * dy); - const offset = Math.sin(distance * (percentage / 100)) * (percentage / 100); + const offset = Math.sin(distance * (amtIn / 100)) * (amtIn / 100); const sourceX = Math.floor(x + offset); const sourceY = Math.floor(y); if (sourceX >= 0 && sourceX < width && sourceY >= 0 && sourceY < height) { @@ -655,11 +619,8 @@ } } } - applyDisplacement(imageData, dispAmt) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; + const { data, width, height} = imageData; const newData = new Uint8ClampedArray(data.length); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { @@ -674,56 +635,133 @@ } data.set(newData); } - - applyPosterize(imageData, percentage) { + applyPosterize(imageData, amtIn) { const data = imageData.data; - const numLevels = Math.max(percentage / 10, 1); + const numLevels = Math.max(amtIn / 10, 1); for (let i = 0; i < data.length; i += 4) { for (let j = 0; j < 3; j++) { data[i + j] = Math.round((data[i + j] * (numLevels - 1)) / 255) * (255 / (numLevels - 1)); } } } + applyBlur(imageData, amtIn) { + const { data, width, height} = imageData; + const radius = Math.floor((amtIn / 100) * 10); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let sum = [0, 0, 0, 0]; + let count = 0; + for (let ky = -radius; ky <= radius; ky++) { + for (let kx = -radius; kx <= radius; kx++) { + const offsetX = x + kx; + const offsetY = y + ky; + if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { + const pixelX = (offsetY * width + offsetX) * 4; + for (let i = 0; i < 4; i++) { sum[i] += data[pixelX + i] } + count++; + } + } + } + const pixelY = (y * width + x) * 4; + if (count > 0) for (let i = 0; i < 4; i++) { data[pixelY + i] = sum[i] / count } + } + } + } + applyScanlines(imageData, amtIn) { + const { data, width, height} = imageData; + for (let y = 0; y < height; y++) { + if (Math.random() < amtIn / 100) { + const scanBright = Math.random() * (amtIn / 2); + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + data[index] = Math.min(data[index] + scanBright, 255); + data[index + 1] = Math.min(data[index + 1] + scanBright, 255); + data[index + 2] = Math.min(data[index + 2] + scanBright, 255); + } + } + } + } + applyGrain(imageData, amtIn) { + const { data, width, height} = imageData; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const index = (y * width + x) * 4; + if (Math.random() < amtIn) { + const grain = Math.floor(Math.random() * amtIn); + data[index] += grain; + data[index + 1] += grain; + data[index + 2] += grain; + } + } + } + } + applyCubism(imageData, amtIn) { + const { data, width, height} = imageData; + const percent = amtIn === 0 ? 1 : Math.abs(amtIn); + for (let y = 0; y < height; y += percent) { + for (let x = 0; x < width; x += percent) { + const endX = Math.min(x + percent, width); + const endY = Math.min(y + percent, height); + const avgColor = [0, 0, 0]; + for (let j = y; j < endY; j++) { + for (let i = x; i < endX; i++) { + const index = (j * width + i) * 4; + avgColor[0] += data[index]; + avgColor[1] += data[index + 1]; + avgColor[2] += data[index + 2]; + } + } + const totalPixels = (endX - x) * (endY - y); + avgColor[0] /= totalPixels; + avgColor[1] /= totalPixels; + avgColor[2] /= totalPixels; + for (let j = y; j < endY; j++) { + for (let i = x; i < endX; i++) { + const index = (j * width + i) * 4; + data[index] = avgColor[0]; + data[index + 1] = avgColor[1]; + data[index + 2] = avgColor[2]; + } + } + } + } + } applyBulgeEffect(args) { return new Promise((resolve) => { - let centerX = Scratch.Cast.toNumber(args.CENTER_X) / 100 || 0; - let centerY = Scratch.Cast.toNumber(args.CENTER_Y) / -100 || 0; - const strength = Scratch.Cast.toNumber(args.STRENGTH) / 100 || 0; + const strength = Scratch.Cast.toNumber(args.STRENGTH) / 100; const img = new Image(); img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - centerX = centerX + img.width / 200; - centerY = centerY + img.height / 200; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); + const canvasSize = Math.max(img.width, img.height) * 2; + let centerX = Scratch.Cast.toNumber(args.CENTER_X) / 100; + let centerY = Scratch.Cast.toNumber(args.CENTER_Y) / -100; + const { canvas, ctx } = this.createCanvasCtx(canvasSize, canvasSize); + const offsetX = (canvas.width - img.width) / 2; + const offsetY = (canvas.height - img.height) / 2; + ctx.drawImage(img, offsetX, offsetY); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyBulge(imageData, centerX, centerY, strength); + this.bulgeApplier(imageData, centerX + 0.5, centerY + 0.5, strength); ctx.putImageData(imageData, 0, 0); - resolve(canvas.toDataURL()); + const bounds = this.getImageBounds(imageData); + const newCanvas = this.createCanvasCtx(bounds.width, bounds.height, canvas, -bounds.offsetX, -bounds.offsetY).canvas; + resolve(newCanvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyBulge(imageData, centerX, centerY, strength) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; + bulgeApplier(imageData, centerX, centerY, strength) { + const { data, width, height } = imageData; const newData = new Uint8ClampedArray(data.length); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const dx = (x / width - centerX) * 2; const dy = (y / height - centerY) * 2; - const distance = Math.sqrt(dx * dx + dy * dy); - const bulge = Math.pow(distance, strength); - const srcX = Math.floor(x + dx * bulge * width - dx * width); - const srcY = Math.floor(y + dy * bulge * height - dy * height); + const bulge = Math.pow(Math.sqrt(dx * dx + dy * dy), strength); + const srcX = Math.floor(x + dx * bulge * width); + const srcY = Math.floor(y + dy * bulge * height); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; - const dstIndex = (y * width + x) * 4; - newData.set(data.subarray(srcIndex, srcIndex + 4), dstIndex); + newData.set(data.subarray(srcIndex, srcIndex + 4), (y * width + x) * 4); } } } @@ -732,34 +770,28 @@ applyWaveEffect(args) { return new Promise((resolve) => { - const amplitudeX = Scratch.Cast.toNumber(args.AMPX) / 10 || 0; - const amplitudeY = Scratch.Cast.toNumber(args.AMPY) / 10 || 0; - const frequencyX = Scratch.Cast.toNumber(args.FREQX) / 100 || 0; - const frequencyY = Scratch.Cast.toNumber(args.FREQY) / 100 || 0; + const ampX = Scratch.Cast.toNumber(args.AMPX) / 10; + const ampY = Scratch.Cast.toNumber(args.AMPY) / 10; + const freqX = Scratch.Cast.toNumber(args.FREQX) / 100; + const freqY = Scratch.Cast.toNumber(args.FREQY) / 100; 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); + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY); + this.waveApplier(imageData, ampX, ampY, freqX, freqY); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyWave(imageData, amplitudeX, amplitudeY, frequencyX, frequencyY) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; + waveApplier(imageData, ampX, ampY, freqX, freqY) { + const { data, width, height} = imageData; const newData = new Uint8ClampedArray(data.length); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { - const srcX = Math.floor(x + amplitudeX * Math.sin(y * frequencyX)); - const srcY = Math.floor(y + amplitudeY * Math.sin(x * frequencyY)); + const srcX = Math.floor(x + ampX * Math.sin(y * freqX)); + const srcY = Math.floor(y + ampY * Math.sin(x * freqY)); if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { const srcIndex = (srcY * width + srcX) * 4; const dstIndex = (y * width + x) * 4; @@ -770,82 +802,29 @@ data.set(newData); } - applyBlur(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - const radius = Math.floor((percentage / 100) * 10); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - let sum = [0, 0, 0, 0]; - let count = 0; - for (let ky = -radius; ky <= radius; ky++) { - for (let kx = -radius; kx <= radius; kx++) { - const offsetX = x + kx; - const offsetY = y + ky; - if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { - const pixelIndex = (offsetY * width + offsetX) * 4; - for (let i = 0; i < 4; i++) { sum[i] += data[pixelIndex + i] } - count++; - } - } - } - const pixelIndex = (y * width + x) * 4; - if (count > 0) for (let i = 0; i < 4; i++) { data[pixelIndex + i] = sum[i] / count } - } - } - } - - applyChunkGlitch(imageData, percentage) { - const newWidth = percentage / 10; - const data = imageData.data; - const imgWidth = imageData.width; - const imgHeight = imageData.height; - const numLines = Math.floor(imgWidth * 1); - for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { - const linePos = Math.floor(Math.random() * imgHeight); - const lineStart = linePos - Math.floor(newWidth / 2); - const lineEnd = lineStart + newWidth; - for (let y = 0; y < imgHeight; y++) { - const srcIndex = (y * imgWidth + linePos) * 4; - if (linePos >= 0 && linePos < imgWidth) { - for (let x = lineStart; x < lineEnd; x++) { - const dstIndex = (y * imgWidth + x) * 4; - data.copyWithin(dstIndex, srcIndex, srcIndex + 4); - } - } - } - } - } - removeTransparencyEffect(args) { return new Promise((resolve) => { - const threshold = Scratch.Cast.toNumber(args.THRESHOLD) / 100 || 0; - const removeUnder = args.REMOVE; + const threshold = Scratch.Cast.toNumber(args.THRESHOLD) / 100; 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); + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyTransparencyRemoval(imageData, threshold, removeUnder); + this.transparncyApplier(imageData, threshold, args.REMOVE); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyTransparencyRemoval(imageData, threshold, removeUnder) { - const data = imageData.data; - const pixelCount = data.length / 4; - for (let i = 0; i < pixelCount; i++) { + transparncyApplier(imageData, threshold, removeUnder) { + const { data, width, height} = imageData; + for (let i = 0; i < width * height; i++) { const alpha = data[i * 4 + 3] / 255; - if ((removeUnder === "under" && alpha < threshold) || - (removeUnder === "over" && alpha > threshold) || + if ( + (removeUnder === "under" && alpha < threshold) || (removeUnder === "over" && alpha > threshold) || (removeUnder === "equal to" && alpha > threshold - 0.01 && - alpha < threshold + 0.01)) { + alpha < threshold + 0.01) + ) { data[i * 4 + 3] = 0; } } @@ -853,41 +832,33 @@ applyLineGlitchEffect(args) { return new Promise((resolve) => { - const percentage = Scratch.Cast.toNumber(args.PERCENTAGE) / 100 || 0; - const direction = args.DIRECT; - const width = Scratch.Cast.toNumber(args.WIDTH) / 50 || 0; + const amtIn = Scratch.Cast.toNumber(args.PERCENTAGE) / 100; + const width = Scratch.Cast.toNumber(args.WIDTH) / 50; 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); + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyLineGlitch(imageData, percentage, direction, width); + this.lineApplier(imageData, amtIn, args.DIRECT, width); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyLineGlitch(imageData, percentage, direction, width) { - const data = imageData.data; - const imgWidth = imageData.width; - const imgHeight = imageData.height; - const numLines = Math.floor(imgHeight * percentage); + lineApplier(imageData, amtIn, direct, widthInp) { + const { data, width, height} = imageData; + const numLines = Math.floor(height * amtIn); for (let lineIndex = 0; lineIndex < numLines; lineIndex++) { - const linePosition = Math.floor(Math.random() * imgHeight); - const lineStart = linePosition - Math.floor(width / 2); - const lineEnd = lineStart + width; - for (let y = (direction === "Y" ? 0 : lineStart); y < (direction === "Y" ? imgHeight : lineEnd); y++) { - for (let x = (direction === "Y" ? lineStart : 0); x < (direction === "Y" ? lineEnd : imgWidth); x++) { - const srcX = (direction === "Y" ? x : linePosition); - const srcY = (direction === "Y" ? linePosition : y); - if (srcX >= 0 && srcX < imgWidth && srcY >= 0 && srcY < imgHeight) { - const srcIndex = (srcY * imgWidth + srcX) * 4; - const dstIndex = (y * imgWidth + x) * 4; - data.copyWithin(dstIndex, srcIndex, srcIndex + 4); + const linePosition = Math.floor(Math.random() * height); + const lineStart = linePosition - Math.floor(widthInp / 2); + const lineEnd = lineStart + widthInp; + for (let y = (direct === "Y" ? 0 : lineStart); y < (direct === "Y" ? height : lineEnd); y++) { + for (let x = (direct === "Y" ? lineStart : 0); x < (direct === "Y" ? lineEnd : width); x++) { + const srcX = (direct === "Y" ? x : linePosition); + const srcY = (direct === "Y" ? linePosition : y); + if (srcX >= 0 && srcX < width && srcY >= 0 && srcY < height) { + const srcIndex = (srcY * width + srcX) * 4; + data.copyWithin((y * width + x) * 4, srcIndex, srcIndex + 4); } } } @@ -896,25 +867,21 @@ applyEdgeOutlineEffect(args) { return new Promise((resolve) => { - const thickness = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); + const thick = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); const color = hexToRgb(args.COLOR); - const a = Math.min(Math.max(args.A, 0), 100) * 2.55; + color[3] = this.clamp(args.A, 0, 100) * 2.55; 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); + const { canvas, ctx } = this.createCanvasCtx(img.width + (thick * 2), img.height + (thick * 2), img, thick, thick); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyOutline(imageData, thickness, color[0], color[1], color[2], a); + this.outlineApplier(imageData, thick, color); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyOutline(imageData, thickness, r, g, b, a) { + outlineApplier(imageData, thick, rgba) { const data = imageData.data; const width = imageData.width; const height = imageData.height; @@ -923,15 +890,14 @@ for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; if (data[index + 3] < 255) { - for (let dy = -thickness; dy <= thickness; dy++) { - for (let dx = -thickness; dx <= thickness; dx++) { + for (let dy = -thick; dy <= thick; dy++) { + for (let dx = -thick; dx <= thick; dx++) { const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const neighborIndex = (ny * width + nx) * 4; - const neighborAlpha = copyData[neighborIndex + 3]; - if (neighborAlpha === 255) { - data.set([r, g, b, a], index); + if (copyData[neighborIndex + 3] === 255) { + data.set(rgba, index); break; } } @@ -942,117 +908,72 @@ } } - applyClipGlitch(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - const percent = percentage / 100; - const numPixelsToEnlarge = Math.floor((percent / 100) * (width * height)); - for (let i = 0; i < numPixelsToEnlarge; i++) { - const x = Math.floor(Math.random() * width); - const y = Math.floor(Math.random() * height); - const index = (y * width + x) * 4; - const enlargeFactor = 1 + Math.random() * (1.5 + percent / 200); - const blurRadius = Math.floor(enlargeFactor * 4); - for (let offsetY = -blurRadius; offsetY <= blurRadius; offsetY++) { - for (let offsetX = -blurRadius; offsetX <= blurRadius; offsetX++) { - const newX = x + offsetX; - const newY = y + offsetY; - const bound = newX >= 0 && newX < width && newY >= 0 && newY < height; - if (bound) data.copyWithin((newY * width + newX) * 4, index, index + 4); - } - } - } + setCutout(args) { this.cutPos = [Scratch.Cast.toNumber(args.X), Scratch.Cast.toNumber(args.Y)] } + changeCutout(args) { + this.cutPos = [this.cutPos[0] + Scratch.Cast.toNumber(args.X), + this.cutPos[1] + Scratch.Cast.toNumber(args.Y)]; } + currentCut(args) { return this.cutPos[args.POS === "X" ? 0 : 1] } - applyScanlines(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - for (let y = 0; y < height; y++) { - if (Math.random() < percentage / 100) { - const scanBright = Math.random() * (percentage / 2); - for (let x = 0; x < width; x++) { - const index = (y * width + x) * 4; - data[index] = Math.min(data[index] + scanBright, 255); - data[index + 1] = Math.min(data[index + 1] + scanBright, 255); - data[index + 2] = Math.min(data[index + 2] + scanBright, 255); - } - } - } + setScale(args) { this.scale = [Scratch.Cast.toNumber(args.SIZE), Scratch.Cast.toNumber(args.Y)] } + changeScale(args) { + this.scale = [this.scale[0] + Scratch.Cast.toNumber(args.SIZE), + this.scale[1] + Scratch.Cast.toNumber(args.Y)]; } + currentScale(args) { return this.scale[args.POS === "X" ? 0 : 1] } - applyGrain(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const index = (y * width + x) * 4; - if (Math.random() < percentage) { - const grain = Math.floor(Math.random() * percentage); - data[index] += grain; - data[index + 1] += grain; - data[index + 2] += grain; - } - } - } + setDirection(args) { this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE) } + changeDirection(args) { + let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); + if (direction > 180) { direction = -180 + Scratch.Cast.toNumber(args.ANGLE) } + if (direction < -180) { direction = 180 + Scratch.Cast.toNumber(args.ANGLE) } + this.cutoutDirection = direction; } + currentDir() { return this.cutoutDirection } - applyCubism(imageData, percentage) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; - const percent = percentage === 0 || percentage === "" ? 1 : Math.abs(Scratch.Cast.toNumber(percentage)); - for (let y = 0; y < height; y += percent) { - for (let x = 0; x < width; x += percent) { - const endX = Math.min(x + percent, width); - const endY = Math.min(y + percent, height); - const avgColor = [0, 0, 0]; - for (let j = y; j < endY; j++) { - for (let i = x; i < endX; i++) { - const index = (j * width + i) * 4; - avgColor[0] += data[index]; - avgColor[1] += data[index + 1]; - avgColor[2] += data[index + 2]; - } - } - const totalPixels = (endX - x) * (endY - y); - avgColor[0] /= totalPixels; - avgColor[1] /= totalPixels; - avgColor[2] /= totalPixels; - for (let j = y; j < endY; j++) { - for (let i = x; i < endX; i++) { - const index = (j * width + i) * 4; - data[index] = avgColor[0]; - data[index + 1] = avgColor[1]; - data[index + 2] = avgColor[2]; - } - } - } - } + maskImage(args) { + return new Promise((resolve) => { + const srcImg = new Image(); + srcImg.onload = () => { + const maskImg = new Image(); + maskImg.onload = () => { + const scaleW = maskImg.width * (this.scale[0] / 50); + const scaleH = maskImg.height * (this.scale[1] / 50); + const isMaskBigger = scaleW >= srcImg.width || scaleH >= srcImg.height; + const cutX = this.cutPos[0] + (srcImg.width / 2) - (scaleW / 2); + const cutY = this.cutPos[1] - (srcImg.height / 2) + (scaleH / 2); + const { canvas, ctx } = this.createCanvasCtx(srcImg.width, srcImg.height); + ctx.drawImage(srcImg, 0, 0); + if (args.TYPE === "clip") ctx.globalCompositeOperation = "destination-in"; + else if (args.TYPE === "mask") ctx.globalCompositeOperation = "destination-out"; + ctx.translate(cutX + scaleW / 2, cutY * -1 + scaleH / 2); + ctx.rotate((this.cutoutDirection - 90) * (Math.PI / 180)); + ctx.drawImage(maskImg, scaleW / -2, scaleH / -2, scaleW, scaleH); + ctx.setTransform(1, 0, 0, 1, 0, 0); + if (args.TYPE === "clip") ctx.globalCompositeOperation = "source-over"; + resolve(canvas.toDataURL("image/png")); + }; + maskImg.src = this.confirmAsset(args.MASK, "png"); + }; + srcImg.src = this.confirmAsset(args.IMG, "png"); + }); } applyAbberationEffect(args) { return new Promise((resolve) => { - const percentage = args.PERCENTAGE; + const amtIn = Scratch.Cast.toNumber(args.PERCENTAGE); const img = new Image(); img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width + Math.abs(percentage) * 5; - canvas.height = img.height + Math.abs(percentage) * 5; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, Math.abs(percentage) * 2.5, Math.abs(percentage) * 2.5); + const { canvas, ctx } = this.createCanvasCtx(img.width + Math.abs(amtIn) * 5, img.height + Math.abs(amtIn) * 5, img, Math.abs(amtIn) * 2.5, Math.abs(amtIn) * 2.5); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - this.applyChromAb(imageData, args.COLOR1, args.COLOR2, - percentage, args.DIRECT); + this.applyChromAb(imageData, args.COLOR1, args.COLOR2, amtIn / 100, args.DIRECT); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; img.src = this.confirmAsset(args.SVG, "png"); }); } - applyChromAb(imageData, color1, color2, percentage, direction) { + applyChromAb(imageData, color1, color2, amtIn, dir) { const data = imageData.data; let width = imageData.width; let height = imageData.height; @@ -1067,26 +988,20 @@ const g = data[srcIndex + 1]; const b = data[srcIndex + 2]; const a = data[srcIndex + 3]; - let newX1, newY1, newX2, newY2; - if (direction === "X") { - newX1 = x + Math.floor((width / 2) * (percentage / 100)); - newY1 = y; - newX2 = x - Math.floor((width / 2) * (percentage / 100)); - newY2 = y; + let newPos1, newPos2; + if (dir === "X") { + newPos1 = [x + Math.floor((width / 2) * amtIn), y]; + newPos2 = [x - Math.floor((width / 2) * amtIn), y]; } else { - newX1 = x; - newY1 = y + Math.floor((height / 2) * (percentage / 100)); - newX2 = x; - newY2 = y - Math.floor((height / 2) * (percentage / 100)); + newPos1 = [x, y + Math.floor((height / 2) * amtIn)]; + newPos2 = [x, y - Math.floor((height / 2) * amtIn)]; } - newX1 = Math.max(0, Math.min(width - 1, newX1)); - newY1 = Math.max(0, Math.min(height - 1, newY1)); - newX2 = Math.max(0, Math.min(width - 1, newX2)); - newY2 = Math.max(0, Math.min(height - 1, newY2)); + newPos1 = [this.clamp(width - 1, 0, newPos1[0]), this.clamp(height - 1, 0, newPos1[1])]; + newPos2 = [this.clamp(width - 1, 0, newPos2[0]), this.clamp(height - 1, 0, newPos2[1])]; const leftColor = [(rgb1[0] * r) / 255, (rgb1[1] * g) / 255, (rgb1[2] * b) / 255]; const rightColor = [(rgb2[0] * r) / 255, (rgb2[1] * g) / 255, (rgb2[2] * b) / 255]; - const leftIndex = (newY1 * width + newX1) * 4; - const rightIndex = (newY2 * width + newX2) * 4; + const leftIndex = (newPos1[1] * width + newPos1[0]) * 4; + const rightIndex = (newPos2[1] * width + newPos2[0]) * 4; for (let i = 0; i < 4; i++) { copy1[leftIndex + i] = leftColor[i]; copy2[rightIndex + i] = rightColor[i]; @@ -1095,7 +1010,7 @@ } } for (let i = 0; i < data.length; i++) { - data[i] = Math.max(0, Math.min(255, (data[i] + copy1[i] + copy2[i]) / 2)); + data[i] = this.clamp((data[i] + copy1[i] + copy2[i]) / 2, 0, 255); } } @@ -1109,14 +1024,10 @@ }); } svgToBitmap(args) { - return this.stretch(this.confirmAsset(args.SVG, "png"), - Math.abs(Scratch.Cast.toNumber(args.WIDTH)), Math.abs(Scratch.Cast.toNumber(args.HEIGHT)) - ); + return this.stretch(this.confirmAsset(args.SVG, "png"), Math.abs(Scratch.Cast.toNumber(args.WIDTH)), Math.abs(Scratch.Cast.toNumber(args.HEIGHT))); } stretchImg(args) { - return this.stretch(this.confirmAsset(args.URI, "png"), - Math.abs(Scratch.Cast.toNumber(args.W)), Math.abs(Scratch.Cast.toNumber(args.H)) - ); + return this.stretch(this.confirmAsset(args.URI, "png"), Math.abs(Scratch.Cast.toNumber(args.W)), Math.abs(Scratch.Cast.toNumber(args.H))); } convertImageToSVG(args) { @@ -1124,10 +1035,7 @@ const img = new Image(); img.src = this.confirmAsset(args.URI, "png"); img.onload = () => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img); ctx.drawImage(img, 0, 0, img.width, img.height); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("version", "1.1"); @@ -1140,12 +1048,10 @@ for (let y = 0; y < img.height; y++) { for (let x = 0; x < img.width; x++) { const colorData = ctx.getImageData(x, y, 1, 1).data; - const alpha = colorData[3]; - if (alpha === 0) continue; + if (colorData[3] === 0) continue; const color = `rgb(${colorData[0]}, ${colorData[1]}, ${colorData[2]})`; - const rightColorData = ctx.getImageData(x + 1, y, 1, 1).data; - const rightColor = `rgb(${rightColorData[0]}, ${rightColorData[1]}, ${rightColorData[2]})`; - if (color === rightColor) { + const pixelColor = ctx.getImageData(x + 1, y, 1, 1).data; + if (color === `rgb(${pixelColor[0]}, ${pixelColor[1]}, ${pixelColor[2]})`) { const mergedPixel = mergedColors.get(color) || {x1: x, y1: y, x2: x + 1, y2: y}; mergedPixel.x2++; mergedColors.set(color, mergedPixel); @@ -1172,7 +1078,7 @@ async makeSVGimage(args) { if (args.URI.startsWith("data:image/")) { - return await new Promise((resolve, reject) => { + return await new Promise((resolve) => { // eslint-disable-next-line const img = new Image(); img.onload = () => { @@ -1180,14 +1086,11 @@ const height = img.height; const svg = ` - + `; + xlink:href="${img.src}"/>`; resolve(args.TYPE === "dataURI" ? `data:image/svg+xml;base64,${btoa(svg)}` : svg); }; - img.onerror = reject; img.src = this.confirmAsset(args.URI, "png"); }); } else { return args.URI } @@ -1198,13 +1101,9 @@ const img = new Image(); img.onload = () => { const pixelData = this.printImg(img); - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); + const { canvas, ctx } = this.createCanvasCtx(img.width, img.height); ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), img.width, img.height), 0, 0); - const percentage = args.NUM * 10 || 0; - const factor = percentage / 100; + const factor = Scratch.Cast.toNumber(args.NUM) / 10; const weights = [0, -factor, 0, -factor, 1 + 4 * factor, -factor, 0, -factor, 0]; this.sharpen(ctx, img.width, img.height, weights, 25); resolve(this.exportImg(img, ctx.getImageData(0, 0, img.width, img.height).data)); @@ -1213,12 +1112,11 @@ }); } sharpen(ctx, width, height, weights, alphaThreshold) { - const imageData = ctx.getImageData(0, 0, width, height); - const data = imageData.data; + const data = ctx.getImageData(0, 0, width, height).data; const side = Math.round(Math.sqrt(weights.length)); const halfSide = Math.floor(side / 2); const output = ctx.createImageData(width, height); - const outputData = output.data; + const outData = output.data; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pixelIndex = (y * width + x) * 4; @@ -1226,33 +1124,29 @@ for (let ky = 0; ky < side; ky++) { for (let kx = 0; kx < side; kx++) { const weight = weights[ky * side + kx]; - const neighborY = Math.min(height - 1, Math.max(0, y + ky - halfSide)); - const neighborX = Math.min(width - 1, Math.max(0, x + kx - halfSide)); - const neighborPixelIndex = (neighborY * width + neighborX) * 4; - r += data[neighborPixelIndex] * weight; - g += data[neighborPixelIndex + 1] * weight; - b += data[neighborPixelIndex + 2] * weight; + const neighborY = this.clamp(y + ky - halfSide, 0, height - 1); + const neighborX = this.clamp(x + kx - halfSide, 0, width - 1); + const neighborPixel = (neighborY * width + neighborX) * 4; + r += data[neighborPixel] * weight; + g += data[neighborPixel + 1] * weight; + b += data[neighborPixel + 2] * weight; } } if (data[pixelIndex + 3] / 255 > alphaThreshold / 50) { - outputData[pixelIndex] = this.clamp(r, 0, 255); - outputData[pixelIndex + 1] = this.clamp(g, 0, 255); - outputData[pixelIndex + 2] = this.clamp(b, 0, 255); - outputData[pixelIndex + 3] = 255; - } else { outputData[pixelIndex + 3] = 0 } + outData[pixelIndex] = this.clamp(r, 0, 255); + outData[pixelIndex + 1] = this.clamp(g, 0, 255); + outData[pixelIndex + 2] = this.clamp(b, 0, 255); + outData[pixelIndex + 3] = 255; + } else { outData[pixelIndex + 3] = 0 } } } ctx.putImageData(output, 0, 0); } - clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } audioToImage(args) { const audioURI = args.AUDIO_URI; const imageWidth = Math.abs(Scratch.Cast.toString(args.W)); - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = imageWidth; - canvas.height = Math.abs(Scratch.Cast.toString(args.H)); + const { canvas, ctx } = this.createCanvasCtx(imageWidth, Math.abs(Scratch.Cast.toString(args.H))); for (let i = 0; i < audioURI.length; i++) { const charCode = audioURI.charCodeAt(i); ctx.fillStyle = `rgb(${(charCode * 2) % 256},${(charCode * 3) % 256},${(charCode * 4) % 256})`; @@ -1269,22 +1163,16 @@ const width = parseFloat(widthMatch[1]); const height = parseFloat(heightMatch[1]); let transform = ""; - if (svg.includes("style=\"transform-origin: center; transform:")) { - svg = svg.replace(/(style="[^"]*transform:[^"]*)/, `$1 skew(${args.Y}deg, ${args.X}deg)`); - } else { - svg = svg.replace( - `width="${width}" height="${height}"`, `width="${width}" height="${height}" style="transform-origin: center; transform: skew(${args.Y}deg, ${args.X}deg)"` - ); - } - const currentTransform = /transform="([^"]*)"/.exec(svg); - const existingTransform = currentTransform ? currentTransform[1] : ""; - const newTransform = existingTransform ? `${existingTransform} ${transform}` : transform; + if (svg.includes("style=\"transform-origin: center; transform:")) svg = svg.replace(/(style="[^"]*transform:[^"]*)/, `$1 skew(${args.Y}deg, ${args.X}deg)`); + else svg = svg.replace(`width="${width}" height="${height}"`, `width="${width}" height="${height}" style="transform-origin: center; transform: skew(${args.Y}deg, ${args.X}deg)"`); + const curTransform = /transform="([^"]*)"/.exec(svg); + const oldTransform = curTransform ? curTransform[1] : ""; + const newTransform = oldTransform ? `${oldTransform} ${transform}` : transform; svg = svg.replace(/transform="([^"]*)"/, `transform="${newTransform}"`); if (args.TYPE === "dataURI") svg = `data:image/svg+xml;base64,${btoa(svg)}`; } return svg; } - updateView(svg, amt) { let values; const viewBoxMatch = svg.match(/viewBox="([^"]+)"/); @@ -1313,8 +1201,7 @@ img.src = this.confirmAsset(args.URI, "png"); return new Promise((resolve) => { img.onload = () => { - const pixelData = this.printImg(img); - resolve(args.TYPE === "total" ? pixelData.length / 4 : args.TYPE === "per line" ? img.width : img.height); + resolve(args.TYPE === "total" ? img.width * img.height : args.TYPE === "per line" ? img.width : img.height); }; }); } @@ -1328,7 +1215,7 @@ const startNum = Scratch.Cast.toNumber(args.NUM); const endNum = Scratch.Cast.toNumber(args.NUM2) || startNum; const pixelData = this.printImg(img); - for (let num = startNum; num <= endNum && num <= pixelData.length / 4; num++) { + for (let num = startNum; num <= endNum && num <= img.width * img.height; num++) { const rgb = hexToRgb(args.COLOR); for (let i = 0; i < 4; i++) { pixelData[((num - 1) * 4) + i] = rgb[i] } } @@ -1343,7 +1230,7 @@ img.onload = () => { const targetPixel = Scratch.Cast.toNumber(args.NUM); const pixelData = this.printImg(img); - if (targetPixel >= 1 && targetPixel <= pixelData.length / 4) { + if (targetPixel >= 1 && targetPixel <= img.width * img.height) { const pixelIndex = (targetPixel - 1) * 4; const rgba = pixelData.slice(pixelIndex, pixelIndex + 4); resolve(rgbaToHex(rgba[0], rgba[1], rgba[2], rgba[3])); @@ -1365,12 +1252,9 @@ if (this.allShards.length >= args.SHARDS) break; for (let j = 0; j < cracks; j++) { if (this.allShards.length >= args.SHARDS) break; - const shardCanvas = document.createElement("canvas"); const shardWidth = newWidth / cracks; const shardHeight = newHeight / cracks; - shardCanvas.width = shardWidth; - shardCanvas.height = shardHeight; - const ctx = shardCanvas.getContext("2d"); + const { canvas, ctx } = this.createCanvasCtx(shardWidth, shardHeight); ctx.clearRect(0, 0, shardWidth, shardHeight); ctx.beginPath(); ctx.moveTo(Math.random() * shardWidth, Math.random() * shardHeight); @@ -1382,8 +1266,7 @@ const offsetX = Math.random() * (newWidth - shardWidth); const offsetY = Math.random() * (newHeight - shardHeight); ctx.drawImage(img, -offsetX, -offsetY, newWidth, newHeight); - const pixelData = this.printImg(shardCanvas); - this.allShards.push(this.exportImg(shardCanvas, pixelData)); + this.allShards.push(this.exportImg(canvas, this.printImg(canvas))); } } resolve(); @@ -1393,29 +1276,10 @@ getShard(args) { return this.allShards[args.SHARD - 1] || "" } - printImg(img, forceWid, forceHei) { - const canvas = document.createElement("canvas"); - canvas.width = forceWid || img.width; - canvas.height = forceHei || img.height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - return ctx.getImageData(0, 0, canvas.width, canvas.height).data; - } - - exportImg(img, pixelData, forceWid, forceHei) { - const canvas = document.createElement("canvas"); - canvas.width = forceWid || img.width; - canvas.height = forceHei || img.height; - const ctx = canvas.getContext("2d"); - ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), canvas.width, canvas.height), 0, 0); - return canvas.toDataURL(); - } - - confirmAsset(input, type) { - if (!input || !(input.startsWith("data:image/") || input.startsWith("{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.confirmAsset(t.CUTOUT,"png")},s.src=this.confirmAsset(t.MAIN,"png")})} + overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.confirmAsset(t.CUTOUT,"png")},s.src=this.confirmAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From e87249f6cb403e8a4841e34a55271a493c124cb0 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:19:51 -0700 Subject: [PATCH 21/26] Image-Effects -- TW Fixes --- extensions/SharkPool/Image-Effects.js | 38 +++------------------------ 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index d9c521a300..02d10ea928 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -384,28 +384,6 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } - }, - // Deprecated - { - opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, - text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, - arguments: { - HEX: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } - } } ], menus: { @@ -414,8 +392,7 @@ PIXELTYPE: ["total", "per line", "per row"], REMOVAL: ["under", "over", "equal to"], fileType: ["content", "dataURI"], - EFFECTS: { acceptReporters: true, items: imageEffectMenu }, - CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated + EFFECTS: { acceptReporters: true, items: imageEffectMenu } }, }; } @@ -473,7 +450,7 @@ return new Promise((resolve) => { const color = hexToRgb(args.COLOR); const img = new Image(); - img.onload = async () => { + img.onload = () => { const pixelData = this.printImg(img); const data = pixelData; for (let i = 0; i < data.length; i += 4) { @@ -550,7 +527,6 @@ applyChunkGlitch(imageData, amtIn) { const { data, width, height} = imageData; const newWidth = amtIn / 10; - const numLines = Math.floor(width * 1); for (let i = 0; i < Math.floor(width * 1); i++) { const linePos = Math.floor(Math.random() * height); const lineStart = linePos - Math.floor(newWidth / 2); @@ -939,7 +915,6 @@ maskImg.onload = () => { const scaleW = maskImg.width * (this.scale[0] / 50); const scaleH = maskImg.height * (this.scale[1] / 50); - const isMaskBigger = scaleW >= srcImg.width || scaleH >= srcImg.height; const cutX = this.cutPos[0] + (srcImg.width / 2) - (scaleW / 2); const cutY = this.cutPos[1] - (srcImg.height / 2) + (scaleH / 2); const { canvas, ctx } = this.createCanvasCtx(srcImg.width, srcImg.height); @@ -1035,7 +1010,7 @@ const img = new Image(); img.src = this.confirmAsset(args.URI, "png"); img.onload = () => { - const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img); + const ctx = this.createCanvasCtx(img.width, img.height, img).ctx; ctx.drawImage(img, 0, 0, img.width, img.height); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("version", "1.1"); @@ -1101,7 +1076,7 @@ const img = new Image(); img.onload = () => { const pixelData = this.printImg(img); - const { canvas, ctx } = this.createCanvasCtx(img.width, img.height); + const ctx = this.createCanvasCtx(img.width, img.height).ctx; ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), img.width, img.height), 0, 0); const factor = Scratch.Cast.toNumber(args.NUM) / 10; const weights = [0, -factor, 0, -factor, 1 + 4 * factor, -factor, 0, -factor, 0]; @@ -1275,11 +1250,6 @@ } getShard(args) { return this.allShards[args.SHARD - 1] || "" } - - // Deprecated - convertHexToRGB(args) { return hexToRgb(args.HEX)[{ R: 0, G: 1, B: 2 }[args.CHANNEL]] || "" } - clipImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.confirmAsset(t.CUTOUT,"png")},s.src=this.confirmAsset(t.MAIN,"png")})} - overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.confirmAsset(t.CUTOUT,"png")},s.src=this.confirmAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From e99cf4b2c854703433164cfab58086ad02030101 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:10:34 -0700 Subject: [PATCH 22/26] Image-Effects -- Simplify and New Block --- extensions/SharkPool/Image-Effects.js | 272 +++++++++++++++----------- 1 file changed, 162 insertions(+), 110 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 02d10ea928..b0934c967c 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -3,26 +3,26 @@ // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool -// Version V.2.4.0 +// Version V.2.4.1 (function (Scratch) { "use strict"; if (!Scratch.extensions.unsandboxed) throw new Error("Image Effects must run unsandboxed"); const menuIconURI = -"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuNzMwNjQiIGhlaWdodD0iMTM1LjczMDY0IiB2aWV3Qm94PSIwLDAsMTM1LjczMDY0LDEzNS43MzA2NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Mi4xMzQ2OCwtMTEyLjEzNDY4KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsPSIjOTk2NmZmIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTc1Ljg4NDY4LDE4MGMwLC0zNS40MDk5MSAyOC43MDU0MSwtNjQuMTE1MzIgNjQuMTE1MzIsLTY0LjExNTMyYzM1LjQwOTkxLDAgNjQuMTE1MzIsMjguNzA1NDEgNjQuMTE1MzIsNjQuMTE1MzJjMCwzNS40MDk5MSAtMjguNzA1NDEsNjQuMTE1MzIgLTY0LjExNTMyLDY0LjExNTMyYy0zNS40MDk5MSwwIC02NC4xMTUzMiwtMjguNzA1NDEgLTY0LjExNTMyLC02NC4xMTUzMnoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiM3NzRkY2IiIHN0cm9rZS13aWR0aD0iNy41Ii8+PHBhdGggZD0iTTI0MC40NDQ3MSwxNzMuMjY3NjJjNC4wOTQ5MiwtMS42NzM4NiA5LjMwMjQ5LC0xLjE5MDEgMTIuMTM5MDIsLTUuMDU1MzljMi45NDAxOSwtNC4wMDY1NSAzLjgyMTg1LC0xMi4zNTQ5MyA0LjQ0ODA0LC0xNy4yNTg2MWMwLjYxMDA3LC00Ljc3NzQ5IDEuNzYyMTIsLTQuNjEwOTMgMi42MzYyMywwLjI0MTA0YzAuOTI1MDIsNS4xMzQ1MyAyLjAzNDk4LDEzLjY3Njk1IDQuNjU0MTcsMTcuMjQ2MDhjMi45OTI4OSw0LjA3ODM3IDguOTIyODMsMy44NDQ2MiAxMy4zMDU3Miw1LjUyMjUyYzMuMDc3MjksMS4xNzgwNyAyLjgwMzA3LDEuODUyNzYgLTAuNTU3NTMsMi41NjMxOGMtNC4zMTUyNSwwLjkxMjIyIC05LjkwMjk2LDEuNjU3MSAtMTIuOTE1ODksNS43NjI3N2MtMy4xNzkwMyw0LjMzMjAyIC00LjI2MTk4LDE0LjY4OTEyIC01LjAwNjEyLDIwLjg0MjU5Yy0wLjUxMDM2LDQuMjIwMjYgLTEuOTQ5MjEsMi40MjI2MiAtMi4yMTYzMiwwLjI3NjAxYy0wLjc3MDA3LC02LjE4ODY3IC0xLjk2NjQ1LC0xNi44MzA3NiAtNS41MjIxNiwtMjEuNjc2MDdjLTIuNTE2NjgsLTMuNDI5NDQgLTYuNjk2MjksLTQuMDQ3ODUgLTEwLjExNzcyLC01LjAwMDM5Yy00LjYxNjIsLTEuMjg1MTggLTUuMTMyNDYsLTEuNzEyMTUgLTAuODQ3NDUsLTMuNDYzNzJ6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjE1Ii8+PHBhdGggZD0iTTI1Mi41ODM3MywxNjguMjEyMjNjMi45NDAxOSwtNC4wMDY1NSAyLjgyMTg1LC0xMi4zNTQ5MiAzLjQ0ODA0LC0xNy4yNTg2YzAuNjEwMDcsLTQuNzc3NDkgMy44NzMyMywtNC42MTA5MyA0Ljc0NzM0LDAuMjQxMDRjMC45MjUwMiw1LjEzNDUzIDAuOTIzODcsMTMuNjc2OTUgMy41NDMwNiwxNy4yNDYwOGMyLjk5Mjg5LDQuMDc4MzcgOC4zNjcyNywyLjczMzUxIDEyLjc1MDE2LDQuNDExNGMzLjA3NzI5LDEuMTc4MDcgMy4zNTg2MywzLjg1Mjc2IC0wLjAwMTk3LDQuNTYzMThjLTQuMzE1MjUsMC45MTIyMiAtOS45MDI5NiwwLjc2ODIxIC0xMi45MTU4OSw0Ljg3Mzg4Yy0zLjE3OTAzLDQuMzMyMDIgLTMuNDg0MiwxNC45MTEzNCAtNC4yMjgzMywyMS4wNjQ4MWMtMC41MTAzNiw0LjIyMDI2IC0zLjcyNjk5LDIuMjAwNCAtMy45OTQxLDAuMDUzNzljLTAuNzcwMDcsLTYuMTg4NjcgLTAuOTY2NDUsLTE2LjgzMDc2IC00LjUyMjE2LC0yMS42NzYwN2MtMi41MTY2OCwtMy40Mjk0NCAtNy40NzQwNywtMy4zODExOCAtMTAuODk1NSwtNC4zMzM3MmMtNC42MTYyLC0xLjI4NTE4IC00LjM1NDY4LC0zLjI2NzcxIC0wLjA2OTY3LC01LjAxOTI4YzAsMCA5LjMwMjQ5LC0wLjMwMTIzIDEyLjEzOTAyLC00LjE2NjUxeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIxNC4xNjUzMSwxNTIuMzM0NzJjMS40OTI4MiwtMS42NDY2OSAxLjk0MDQ3LC01LjA3Nzg3IDIuMjU4NCwtNy4wOTMyOGMwLjMwOTc1LC0xLjk2MzU1IDAuODk0NjcsLTEuODk1MDkgMS4zMzg0OSwwLjA5OTA2YzAuNDY5NjYsMi4xMTAyOSAxLjAzMzIyLDUuNjIxMjMgMi4zNjMwNiw3LjA4ODE0YzEuNTE5NTgsMS42NzYyMSA0LjUzMDM3LDEuNTgwMTMgNi43NTU3LDIuMjY5NzVjMS41NjI0MywwLjQ4NDE4IDEuNDIzMiwwLjc2MTQ4IC0wLjI4MzA3LDEuMDUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjY4MTA3IC02LjU1Nzc2LDIuMzY4NWMtMS42MTQwOCwxLjc4MDQ2IC0yLjE2MzkzLDYuMDM3MjIgLTIuNTQxNzUsOC41NjYyOWMtMC4yNTkxMiwxLjczNDUyIC0wLjk4OTY3LDAuOTk1NyAtMS4xMjUyOSwwLjExMzQ0Yy0wLjM5MDk5LC0yLjU0MzU0IC0wLjk5ODQyLC02LjkxNzQ0IC0yLjgwMzc2LC04LjkwODg2Yy0xLjI3Nzc5LC0xLjQwOTUgLTMuMzk5OSwtMS42NjM2NyAtNS4xMzcwNiwtMi4wNTUxNmMtMi4zNDM3NywtMC41MjgyMSAtMi42MDU5LC0wLjcwMzcgLTAuNDMwMjcsLTEuNDIzNTljMCwwIDQuNzIzMTMsLTAuNDg5MTIgNi4xNjMzMSwtMi4wNzc3NXoiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjE0LjE2NTMxLDE1Mi4zMzQ3MmMxLjQ5MjgyLC0xLjY0NjY5IDEuMjczODEsLTQuOTY2NzYgMS41OTE3NCwtNi45ODIxN2MwLjMwOTc1LC0xLjk2MzU1IDIuMTE2ODksLTIuMDA2MiAyLjU2MDcsLTAuMDEyMDVjMC40Njk2NiwyLjExMDI5IDAuNDc3NjcsNS42MjEyMyAxLjgwNzUxLDcuMDg4MTRjMS41MTk1OCwxLjY3NjIxIDQuMzA4MTUsMC42MzU2OCA2LjUzMzQ4LDEuMzI1M2MxLjU2MjQzLDAuNDg0MTggMS42NDU0MiwyLjI2MTQ4IC0wLjA2MDg1LDIuNTUzNDZjLTIuMTkwOTgsMC4zNzQ5MiAtNS4wMjgwMSwwLjEyNTUyIC02LjU1Nzc2LDEuODEyOTVjLTEuNjE0MDgsMS43ODA0NiAtMS4xNjM5Myw2LjAzNzIyIC0xLjU0MTc1LDguNTY2MjljLTAuMjU5MTIsMS43MzQ1MiAtMi43Njc0NSwwLjk5NTcgLTIuOTAzMDcsMC4xMTM0NGMtMC4zOTA5OSwtMi41NDM1NCAtMC4yMjA2NCwtNi45MTc0NCAtMi4wMjU5OCwtOC45MDg4NmMtMS4yNzc3OSwtMS40MDk1IC00LjI4ODc5LC0wLjk5NyAtNi4wMjU5NSwtMS4zODg0OWMtMi4wMDQ1OCwtMC40NTE3NyAtMS4yNjQyMSwtMi4yMDEwOCAtMC4wMDA1MiwtMi42OTgwOWMwLjIxMzgyLC0wLjA4NDA5IDUuMTgyMjgsMC4xMTg3MSA2LjYyMjQ2LC0xLjQ2OTkyeiIgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aW5kZXgmcXVvdDs6bnVsbH0iIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwIi8+PHBhdGggZD0iTTIyMC44NTY0LDIwNC44MDk1MWMxLjU2MTg4LC0xLjk1NjIgMi4wMzAyNSwtNi4wMzIyOSAyLjM2Mjg5LC04LjQyNjUyYzAuMzI0MDgsLTIuMzMyNjIgMC45MzYwNiwtMi4yNTEyOSAxLjQwMDQxLDAuMTE3NjhjMC40OTEzOSwyLjUwNjkzIDEuMDgxMDIsNi42Nzc3OSAyLjQ3MjM5LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuNzM5OTgsMS44NzcxNCA3LjA2ODI1LDIuNjk2MzdjMS42MzQ3MiwwLjU3NTIgMS40ODkwNSwwLjkwNDYxIC0wLjI5NjE3LDEuMjUxNDdjLTIuMjkyMzUsMC40NDUzOCAtNS4yNjA2NCwwLjgwOTA4IC02Ljg2MTE2LDIuODEzNjhjLTEuNjg4NzUsMi4xMTUxMSAtMi4yNjQwNSw3LjE3MTk4IC0yLjY1OTM1LDEwLjE3NjQxYy0wLjI3MTExLDIuMDYwNTQgLTEuMDM1NDUsMS4xODI4NSAtMS4xNzczNSwwLjEzNDc2Yy0wLjQwOTA4LC0zLjAyMTYyIC0xLjA0NDYxLC04LjIxNzY0IC0yLjkzMzQ4LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtMy41NTcxOSwtMS45NzYzNyAtNS4zNzQ3MSwtMi40NDE0NWMtMi40NTIyLC0wLjYyNzQ5IC0yLjcyNjQ2LC0wLjgzNTk1IC0wLjQ1MDE5LC0xLjY5MTE2YzAsMCA0Ljk0MTY1LC0wLjU4MTA3IDYuNDQ4NDYsLTIuNDY4M3oiIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2luZGV4JnF1b3Q7Om51bGx9IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMjIwLjg1NjQsMjA0LjgwOTUxYzEuNTYxODgsLTEuOTU2MiAxLjM2MzU4LC02LjAzMjI5IDEuNjk2MjIsLTguNDI2NTJjMC4zMjQwOCwtMi4zMzI2MiAyLjM4MDUsLTIuMjUxMjkgMi44NDQ4NiwwLjExNzY4YzAuNDkxMzksMi41MDY5MyAwLjMwMzI0LDYuNjc3NzkgMS42OTQ2LDguNDIwNDFjMS41ODk4OSwxLjk5MTI2IDQuMjk1NTMsMC45NDY1OCA2LjYyMzgxLDEuNzY1ODJjMS42MzQ3MiwwLjU3NTIgMS45MzM0OSwyLjM5MDcyIDAuMTQ4MjgsMi43Mzc1OGMtMi4yOTIzNSwwLjQ0NTM4IC01LjI2MDY0LDAuMjUzNTIgLTYuODYxMTYsMi4yNTgxM2MtMS42ODg3NSwyLjExNTExIC0xLjQ4NjI3LDcuMTcxOTggLTEuODgxNTYsMTAuMTc2NDFjLTAuMjcxMTEsMi4wNjA1NCAtMi41OTEwMSwxLjE4Mjg1IC0yLjczMjksMC4xMzQ3NmMtMC40MDkwOCwtMy4wMjE2MiAtMC4yNjY4MywtOC4yMTc2NCAtMi4xNTU3LC0xMC41ODMzNmMtMS4zMzY5MSwtMS42NzQ0MiAtNC4wMDE2NCwtMS41MzE5MyAtNS44MTkxNiwtMS45OTcwMWMtMi40NTIyLC0wLjYyNzQ5IC0yLjI4MjAxLC0xLjgzNTk1IC0wLjAwNTc0LC0yLjY5MTE2YzAsMCA0Ljk0MTY0LC0wLjAyNTUyIDYuNDQ4NDUsLTEuOTEyNzV6IiBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpbmRleCZxdW90OzpudWxsfSIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjAiLz48L2c+PC9nPjwvc3ZnPg=="; +"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzUuNzMxIiBoZWlnaHQ9IjEzNS43MzEiIHZpZXdCb3g9IjAgMCAxMzUuNzMxIDEzNS43MzEiPjxnIGZpbGw9IiM5NmYiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTMuNzUgNjcuODY1YzAtMzUuNDEgMjguNzA1LTY0LjExNSA2NC4xMTUtNjQuMTE1czY0LjExNSAyOC43MDUgNjQuMTE1IDY0LjExNS0yOC43MDUgNjQuMTE1LTY0LjExNSA2NC4xMTVTMy43NSAxMDMuMjc1IDMuNzUgNjcuODY1eiIgc3Ryb2tlPSIjNzc0ZGNiIiBzdHJva2Utd2lkdGg9IjcuNSIvPjxwYXRoIGQ9Ik02OC4zMSA2MS4xMzNjNC4wOTUtMS42NzQgOS4zMDItMS4xOSAxMi4xMzktNS4wNTYgMi45NC00LjAwNiAzLjgyMi0xMi4zNTUgNC40NDgtMTcuMjU4LjYxLTQuNzc4IDEuNzYyLTQuNjExIDIuNjM2LjI0LjkyNSA1LjEzNSAyLjAzNSAxMy42NzggNC42NTQgMTcuMjQ3IDIuOTkzIDQuMDc4IDguOTIzIDMuODQ0IDEzLjMwNiA1LjUyMiAzLjA3NyAxLjE3OCAyLjgwMyAxLjg1My0uNTU4IDIuNTYzLTQuMzE1LjkxMy05LjkwMyAxLjY1OC0xMi45MTYgNS43NjMtMy4xNzkgNC4zMzItNC4yNjIgMTQuNjktNS4wMDYgMjAuODQzLS41MSA0LjIyLTEuOTQ5IDIuNDIyLTIuMjE2LjI3Ni0uNzctNi4xODktMS45NjYtMTYuODMxLTUuNTIyLTIxLjY3Ni0yLjUxNy0zLjQzLTYuNjk2LTQuMDQ4LTEwLjExOC01LTQuNjE2LTEuMjg2LTUuMTMyLTEuNzEzLS44NDctMy40NjR6IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMTUiLz48cGF0aCBkPSJNODAuNDQ5IDU2LjA3N2MyLjk0LTQuMDA2IDIuODIyLTEyLjM1NSAzLjQ0OC0xNy4yNTguNjEtNC43NzggMy44NzMtNC42MTEgNC43NDcuMjQuOTI1IDUuMTM1LjkyNCAxMy42NzggMy41NDMgMTcuMjQ3IDIuOTkzIDQuMDc4IDguMzY3IDIuNzMzIDEyLjc1IDQuNDExIDMuMDc4IDEuMTc4IDMuMzU5IDMuODUzLS4wMDIgNC41NjMtNC4zMTUuOTEzLTkuOTAzLjc2OS0xMi45MTYgNC44NzQtMy4xNzkgNC4zMzItMy40ODQgMTQuOTEyLTQuMjI4IDIxLjA2NS0uNTEgNC4yMi0zLjcyNyAyLjItMy45OTQuMDU0LS43Ny02LjE4OS0uOTY2LTE2LjgzMS00LjUyMi0yMS42NzYtMi41MTctMy40My03LjQ3NC0zLjM4MS0xMC44OTYtNC4zMzQtNC42MTYtMS4yODUtNC4zNTQtMy4yNjgtLjA3LTUuMDIgMCAwIDkuMzAzLS4zIDEyLjE0LTQuMTY2Ii8+PHBhdGggZD0iTTQyLjAzIDQwLjJjMS40OTMtMS42NDcgMS45NC01LjA3OCAyLjI1OS03LjA5NC4zMS0xLjk2My44OTQtMS44OTUgMS4zMzguMS40NyAyLjExIDEuMDMzIDUuNjIgMi4zNjMgNy4wODggMS41MiAxLjY3NiA0LjUzIDEuNTggNi43NTYgMi4yNyAxLjU2Mi40ODQgMS40MjMuNzYtLjI4MyAxLjA1My0yLjE5MS4zNzUtNS4wMjguNjgtNi41NTggMi4zNjgtMS42MTQgMS43OC0yLjE2NCA2LjAzOC0yLjU0MiA4LjU2Ny0uMjU5IDEuNzM0LS45OS45OTUtMS4xMjUuMTEzLS4zOS0yLjU0My0uOTk4LTYuOTE3LTIuODA0LTguOTA5LTEuMjc3LTEuNDEtMy40LTEuNjYzLTUuMTM3LTIuMDU1LTIuMzQ0LS41MjgtMi42MDYtLjcwNC0uNDMtMS40MjQgMCAwIDQuNzIzLS40ODkgNi4xNjMtMi4wNzd6IiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNNDIuMDMgNDAuMmMxLjQ5My0xLjY0NyAxLjI3NC00Ljk2NyAxLjU5Mi02Ljk4Mi4zMS0xLjk2NCAyLjExNy0yLjAwNyAyLjU2LS4wMTMuNDcgMi4xMS40NzggNS42MjIgMS44MDggNy4wODkgMS41MiAxLjY3NiA0LjMwOC42MzUgNi41MzQgMS4zMjUgMS41NjIuNDg0IDEuNjQ1IDIuMjYxLS4wNjEgMi41NTMtMi4xOTEuMzc1LTUuMDI4LjEyNi02LjU1OCAxLjgxMy0xLjYxNCAxLjc4LTEuMTY0IDYuMDM4LTEuNTQyIDguNTY3LS4yNTkgMS43MzQtMi43NjcuOTk1LTIuOTAzLjExMy0uMzktMi41NDMtLjIyLTYuOTE3LTIuMDI2LTguOTA5LTEuMjc3LTEuNDEtNC4yODgtLjk5Ny02LjAyNi0xLjM4OC0yLjAwNC0uNDUyLTEuMjY0LTIuMjAxIDAtMi42OTguMjE0LS4wODQgNS4xODIuMTE4IDYuNjIyLTEuNDciLz48cGF0aCBkPSJNNDguNzIxIDkyLjY3NWMxLjU2Mi0xLjk1NyAyLjAzLTYuMDMzIDIuMzYzLTguNDI3LjMyNC0yLjMzMy45MzYtMi4yNTEgMS40LjExOC40OTIgMi41MDcgMS4wODIgNi42NzcgMi40NzMgOC40MiAxLjU5IDEuOTkxIDQuNzQgMS44NzcgNy4wNjggMi42OTYgMS42MzUuNTc2IDEuNDkuOTA1LS4yOTYgMS4yNTItMi4yOTIuNDQ1LTUuMjYuODA5LTYuODYxIDIuODE0LTEuNjg5IDIuMTE1LTIuMjY0IDcuMTcyLTIuNjYgMTAuMTc2LS4yNyAyLjA2LTEuMDM1IDEuMTgzLTEuMTc3LjEzNS0uNDA5LTMuMDIyLTEuMDQ0LTguMjE4LTIuOTMzLTEwLjU4NC0xLjMzNy0xLjY3NC0zLjU1Ny0xLjk3Ni01LjM3NS0yLjQ0MS0yLjQ1Mi0uNjI4LTIuNzI2LS44MzYtLjQ1LTEuNjkxIDAgMCA0Ljk0Mi0uNTgxIDYuNDQ4LTIuNDY4eiIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTQ4LjcyMSA5Mi42NzVjMS41NjItMS45NTcgMS4zNjQtNi4wMzMgMS42OTctOC40MjcuMzI0LTIuMzMzIDIuMzgtMi4yNTEgMi44NDQuMTE4LjQ5MiAyLjUwNy4zMDQgNi42NzcgMS42OTUgOC40MiAxLjU5IDEuOTkxIDQuMjk2Ljk0NyA2LjYyNCAxLjc2NiAxLjYzNS41NzUgMS45MzMgMi4zOS4xNDggMi43MzctMi4yOTIuNDQ2LTUuMjYuMjU0LTYuODYxIDIuMjU5LTEuNjg5IDIuMTE1LTEuNDg2IDcuMTcyLTEuODgyIDEwLjE3Ni0uMjcgMi4wNi0yLjU5IDEuMTgzLTIuNzMyLjEzNS0uNDEtMy4wMjItLjI2Ny04LjIxOC0yLjE1Ni0xMC41ODQtMS4zMzctMS42NzQtNC4wMDItMS41MzItNS44Mi0xLjk5Ny0yLjQ1Mi0uNjI3LTIuMjgxLTEuODM2LS4wMDUtMi42OSAwIDAgNC45NDItLjAyNiA2LjQ0OC0xLjkxNHoiLz48L2c+PC9zdmc+"; - function hexToRgb(hex) { - // returns [r,g,b,a] + const cast = Scratch.Cast; + const hexToRgb = (hex) => { return [ parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16), - hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 255 + hex.length === 9 ? parseInt(hex.slice(7, 9), 16) : 255 ]; - } - function rgbaToHex(r, g, b, a) { + }; + const rgbaToHex = (r, g, b, a) => { const alpha = a !== undefined ? Math.round(a).toString(16).padStart(2, "0") : ""; return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}${alpha}`; - } + }; const imageEffectMenu = [ "Saturation", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", "Vignette", "Ripple", "Displacement", "Posterize", "Blur", "Scanlines", "Grain", "Cubism", @@ -49,7 +49,7 @@ blockType: Scratch.BlockType.REPORTER, text: "apply hue [COLOR] to URI [SVG]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, COLOR: { type: Scratch.ArgumentType.COLOR } } }, @@ -60,7 +60,7 @@ text: "remove color [COLOR] from [DATA_URI]", arguments: { COLOR: { type: Scratch.ArgumentType.COLOR }, - DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } }, { @@ -70,7 +70,7 @@ arguments: { COLOR: { type: Scratch.ArgumentType.COLOR }, REPLACE: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00ff00" }, - DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } + DATA_URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } } }, { @@ -88,7 +88,7 @@ text: "set [EFFECT] effect of URI [SVG] to [PERCENTAGE]%", arguments: { EFFECT: { type: Scratch.ArgumentType.STRING, menu: "EFFECTS" }, - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 } } }, @@ -97,7 +97,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set bulge effect of URI [SVG] to [STRENGTH]% at x [CENTER_X] y [CENTER_Y]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, STRENGTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, CENTER_X: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 }, CENTER_Y: { type: Scratch.ArgumentType.NUMBER, defaultValue: 0 } @@ -108,7 +108,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set wave effect of URI [SVG] to amplitude x [AMPX] y [AMPY] and frequency x [FREQX] y [FREQY]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, AMPX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, AMPY: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, FREQX: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, @@ -120,7 +120,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set line glitch of URI [SVG] to [PERCENTAGE]% on [DIRECT] axis and line width [WIDTH]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, DIRECT: { type: Scratch.ArgumentType.STRING, menu: "POSITIONS" }, WIDTH: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } @@ -131,7 +131,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set abberation of URI [SVG] to colors [COLOR1] and [COLOR2] at [PERCENTAGE]% on [DIRECT] axis", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, PERCENTAGE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }, COLOR1: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, COLOR2: { type: Scratch.ArgumentType.COLOR, defaultValue: "#00f7ff" }, @@ -144,7 +144,7 @@ blockType: Scratch.BlockType.REPORTER, text: "remove pixels from URI [SVG] [REMOVE] [THRESHOLD]% transparency", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, THRESHOLD: { type: Scratch.ArgumentType.NUMBER, defaultValue: 50 }, REMOVE: { type: Scratch.ArgumentType.STRING, menu: "REMOVAL" } } @@ -152,12 +152,11 @@ { opcode: "applyEdgeOutlineEffect", blockType: Scratch.BlockType.REPORTER, - text: "add outline to URI [SVG] thickness [THICKNESS] color [COLOR] opacity [A]%", + text: "add outline to URI [SVG] thickness [THICKNESS] color [COLOR]", arguments: { - SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, THICKNESS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, - COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" }, - A: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } + COLOR: { type: Scratch.ArgumentType.COLOR, defaultValue: "#ff0000" } } }, { blockType: Scratch.BlockType.LABEL, text: "Clipping" }, @@ -254,7 +253,7 @@ blockType: Scratch.BlockType.COMMAND, text: "crack [URI] into [SHARDS] shards", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, SHARDS: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } } }, @@ -267,12 +266,22 @@ } }, { blockType: Scratch.BlockType.LABEL, text: "Pixels" }, + { + opcode: "commonCol", + blockType: Scratch.BlockType.REPORTER, + text: "[TYPE] common color in [URI]", + arguments: { + TYPE: { type: Scratch.ArgumentType.STRING, menu: "DOMINANT" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" } + } + }, + "---", { opcode: "numPixels", blockType: Scratch.BlockType.REPORTER, text: "number of pixels [TYPE] in [URI]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "PIXELTYPE" } } }, @@ -281,7 +290,7 @@ blockType: Scratch.BlockType.REPORTER, text: "get hex of pixel #[NUM] in [URI]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 } } }, @@ -290,7 +299,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set color of pixel #[NUM] to [COLOR] in [URI]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, COLOR: { type: Scratch.ArgumentType.COLOR } } @@ -300,7 +309,7 @@ blockType: Scratch.BlockType.REPORTER, text: "set color of pixels from #[NUM] to [NUM2] to [COLOR] in [URI]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 1 }, NUM2: { type: Scratch.ArgumentType.NUMBER, defaultValue: 10 }, COLOR: { type: Scratch.ArgumentType.COLOR } @@ -322,7 +331,7 @@ blockType: Scratch.BlockType.REPORTER, text: "convert bitmap URI [URI] to svg [TYPE]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } } }, @@ -331,7 +340,7 @@ blockType: Scratch.BlockType.REPORTER, text: "make svg with image URI [URI] to svg [TYPE]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, TYPE: { type: Scratch.ArgumentType.STRING, menu: "fileType" } } }, @@ -340,7 +349,7 @@ blockType: Scratch.BlockType.REPORTER, text: "upscale image URI [URI] by [NUM] %", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, NUM: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 } } }, @@ -350,7 +359,7 @@ blockType: Scratch.BlockType.REPORTER, text: "stretch URI [URI] to width [W] height [H]", arguments: { - URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, + URI: { type: Scratch.ArgumentType.STRING, defaultValue: "svg/data-uri" }, W: { type: Scratch.ArgumentType.NUMBER, defaultValue: 200 }, H: { type: Scratch.ArgumentType.NUMBER, defaultValue: 100 } } @@ -384,6 +393,28 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } + }, + // Deprecated + { + opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, + text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, + arguments: { + HEX: { type: Scratch.ArgumentType.COLOR }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } + } } ], menus: { @@ -391,8 +422,10 @@ MASKING: ["clip", "mask", "overlay"], PIXELTYPE: ["total", "per line", "per row"], REMOVAL: ["under", "over", "equal to"], + DOMINANT: ["most", "least"], fileType: ["content", "dataURI"], - EFFECTS: { acceptReporters: true, items: imageEffectMenu } + EFFECTS: { acceptReporters: true, items: imageEffectMenu }, + CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated }, }; } @@ -414,8 +447,7 @@ createCanvasCtx(w, h, img, x, y) { const canvas = document.createElement("canvas"); - canvas.width = w; - canvas.height = h; + canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); if (img !== undefined) ctx.drawImage(img, x, y); return { canvas, ctx }; @@ -437,36 +469,34 @@ return { width: maxX - minX + 1, height: maxY - minY + 1, offsetX: minX, offsetY: minY }; } - confirmAsset(input, type) { + convertAsset(input, type) { if (!input || !(input.startsWith("data:image/") || input.startsWith(" { const color = hexToRgb(args.COLOR); const img = new Image(); img.onload = () => { - const pixelData = this.printImg(img); - const data = pixelData; + const data = this.printImg(img); for (let i = 0; i < data.length; i += 4) { data[i] = Math.min(255, (data[i] * color[0]) / 255); data[i + 1] = Math.min(255, (data[i + 1] * color[1]) / 255); data[i + 2] = Math.min(255, (data[i + 2] * color[2]) / 255); + data[i + 3] = Math.min(255, (data[i + 3] * (color[3] ?? 255)) / 255); } - resolve(this.exportImg(img, pixelData)); + resolve(this.exportImg(img, data)); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } - deleteColor(args) { - return this.replaceColor({ COLOR : args.COLOR, REPLACE : "#00000000", DATA_URI : args.DATA_URI }); - } + deleteColor(args) { return this.replaceColor({ ...args, REPLACE : "#00000000" }) } replaceColor(args) { const colRem = hexToRgb(args.COLOR); @@ -482,24 +512,24 @@ } resolve(this.exportImg(imageElement, pixelData)); }; - imageElement.src = this.confirmAsset(args.DATA_URI, "png"); + imageElement.src = this.convertAsset(args.DATA_URI, "png"); }); } applyEffect(args) { return new Promise((resolve) => { - const percent = Scratch.Cast.toNumber(args.PERCENTAGE); + const percent = cast.toNumber(args.PERCENTAGE); const img = new Image(); img.onload = async () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const effectFunc = this[`apply${Scratch.Cast.toString(args.EFFECT).replaceAll(" ", "")}`]; + const effectFunc = this[`apply${cast.toString(args.EFFECT).replaceAll(" ", "")}`]; if (effectFunc && typeof effectFunc === "function") await effectFunc(imageData, percent); else resolve(""); ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } applySaturation(imageData, amtIn) { @@ -513,14 +543,14 @@ applyOpaque(imageData, amtIn) { const data = imageData.data; amtIn = Math.max((amtIn + 100) / 100, 0); - for (let i = 0; i < data.length; i += 4) { data[i + 3] = data[i + 3] * amtIn } + for (let i = 0; i < data.length; i += 4) data[i + 3] = data[i + 3] * amtIn; } applyGlitch(imageData, amtIn) { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { if (Math.random() * 100 <= amtIn) { const rnd = () => (Math.random() - 0.5) * amtIn * 3; - for (let j = 0; j < 3; j++) { data[i + j] = (data[i + j] + rnd()) % 256 } + for (let j = 0; j < 3; j++) data[i + j] = (data[i + j] + rnd()) % 256; } } } @@ -572,7 +602,7 @@ const distance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); let vigAMT = (amtIn < 0) ? 1 - (distance / maxDistance) * (amtIn / 100) : ((maxDistance - distance) / maxDistance) * (amtIn / 100); vigAMT = Math.max(0, Math.min(1, vigAMT)); - for (let i = 0; i < 3; i++) { data[index + i] = Math.round(data[index + i] * vigAMT) } + for (let i = 0; i < 3; i++) data[index + i] = Math.round(data[index + i] * vigAMT); } } } @@ -633,13 +663,13 @@ const offsetY = y + ky; if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { const pixelX = (offsetY * width + offsetX) * 4; - for (let i = 0; i < 4; i++) { sum[i] += data[pixelX + i] } + for (let i = 0; i < 4; i++) sum[i] += data[pixelX + i]; count++; } } } const pixelY = (y * width + x) * 4; - if (count > 0) for (let i = 0; i < 4; i++) { data[pixelY + i] = sum[i] / count } + if (count > 0) for (let i = 0; i < 4; i++) data[pixelY + i] = sum[i] / count; } } } @@ -705,12 +735,12 @@ applyBulgeEffect(args) { return new Promise((resolve) => { - const strength = Scratch.Cast.toNumber(args.STRENGTH) / 100; + const strength = cast.toNumber(args.STRENGTH) / 100; const img = new Image(); img.onload = () => { const canvasSize = Math.max(img.width, img.height) * 2; - let centerX = Scratch.Cast.toNumber(args.CENTER_X) / 100; - let centerY = Scratch.Cast.toNumber(args.CENTER_Y) / -100; + let centerX = cast.toNumber(args.CENTER_X) / 100; + let centerY = cast.toNumber(args.CENTER_Y) / -100; const { canvas, ctx } = this.createCanvasCtx(canvasSize, canvasSize); const offsetX = (canvas.width - img.width) / 2; const offsetY = (canvas.height - img.height) / 2; @@ -722,7 +752,7 @@ const newCanvas = this.createCanvasCtx(bounds.width, bounds.height, canvas, -bounds.offsetX, -bounds.offsetY).canvas; resolve(newCanvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } bulgeApplier(imageData, centerX, centerY, strength) { @@ -746,10 +776,10 @@ applyWaveEffect(args) { return new Promise((resolve) => { - const ampX = Scratch.Cast.toNumber(args.AMPX) / 10; - const ampY = Scratch.Cast.toNumber(args.AMPY) / 10; - const freqX = Scratch.Cast.toNumber(args.FREQX) / 100; - const freqY = Scratch.Cast.toNumber(args.FREQY) / 100; + const ampX = cast.toNumber(args.AMPX) / 10; + const ampY = cast.toNumber(args.AMPY) / 10; + const freqX = cast.toNumber(args.FREQX) / 100; + const freqY = cast.toNumber(args.FREQY) / 100; const img = new Image(); img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); @@ -758,7 +788,7 @@ ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } waveApplier(imageData, ampX, ampY, freqX, freqY) { @@ -780,7 +810,7 @@ removeTransparencyEffect(args) { return new Promise((resolve) => { - const threshold = Scratch.Cast.toNumber(args.THRESHOLD) / 100; + const threshold = cast.toNumber(args.THRESHOLD) / 100; const img = new Image(); img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); @@ -789,7 +819,7 @@ ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } transparncyApplier(imageData, threshold, removeUnder) { @@ -808,8 +838,8 @@ applyLineGlitchEffect(args) { return new Promise((resolve) => { - const amtIn = Scratch.Cast.toNumber(args.PERCENTAGE) / 100; - const width = Scratch.Cast.toNumber(args.WIDTH) / 50; + const amtIn = cast.toNumber(args.PERCENTAGE) / 100; + const width = cast.toNumber(args.WIDTH) / 50; const img = new Image(); img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); @@ -818,7 +848,7 @@ ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } lineApplier(imageData, amtIn, direct, widthInp) { @@ -843,9 +873,8 @@ applyEdgeOutlineEffect(args) { return new Promise((resolve) => { - const thick = Math.ceil(Scratch.Cast.toNumber(args.THICKNESS) / 4); + const thick = Math.ceil(cast.toNumber(args.THICKNESS) / 4); const color = hexToRgb(args.COLOR); - color[3] = this.clamp(args.A, 0, 100) * 2.55; const img = new Image(); img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width + (thick * 2), img.height + (thick * 2), img, thick, thick); @@ -854,13 +883,11 @@ ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } outlineApplier(imageData, thick, rgba) { - const data = imageData.data; - const width = imageData.width; - const height = imageData.height; + let { data, width, height } = imageData; const copyData = new Uint8ClampedArray(data); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { @@ -884,25 +911,25 @@ } } - setCutout(args) { this.cutPos = [Scratch.Cast.toNumber(args.X), Scratch.Cast.toNumber(args.Y)] } + setCutout(args) { this.cutPos = [cast.toNumber(args.X), cast.toNumber(args.Y)] } changeCutout(args) { - this.cutPos = [this.cutPos[0] + Scratch.Cast.toNumber(args.X), - this.cutPos[1] + Scratch.Cast.toNumber(args.Y)]; + this.cutPos = [this.cutPos[0] + cast.toNumber(args.X), + this.cutPos[1] + cast.toNumber(args.Y)]; } currentCut(args) { return this.cutPos[args.POS === "X" ? 0 : 1] } - setScale(args) { this.scale = [Scratch.Cast.toNumber(args.SIZE), Scratch.Cast.toNumber(args.Y)] } + setScale(args) { this.scale = [cast.toNumber(args.SIZE), cast.toNumber(args.Y)] } changeScale(args) { - this.scale = [this.scale[0] + Scratch.Cast.toNumber(args.SIZE), - this.scale[1] + Scratch.Cast.toNumber(args.Y)]; + this.scale = [this.scale[0] + cast.toNumber(args.SIZE), + this.scale[1] + cast.toNumber(args.Y)]; } currentScale(args) { return this.scale[args.POS === "X" ? 0 : 1] } - setDirection(args) { this.cutoutDirection = Scratch.Cast.toNumber(args.ANGLE) } + setDirection(args) { this.cutoutDirection = cast.toNumber(args.ANGLE) } changeDirection(args) { - let direction = this.cutoutDirection + Scratch.Cast.toNumber(args.ANGLE); - if (direction > 180) { direction = -180 + Scratch.Cast.toNumber(args.ANGLE) } - if (direction < -180) { direction = 180 + Scratch.Cast.toNumber(args.ANGLE) } + let direction = this.cutoutDirection + cast.toNumber(args.ANGLE); + if (direction > 180) { direction = -180 + cast.toNumber(args.ANGLE) } + if (direction < -180) { direction = 180 + cast.toNumber(args.ANGLE) } this.cutoutDirection = direction; } currentDir() { return this.cutoutDirection } @@ -928,15 +955,15 @@ if (args.TYPE === "clip") ctx.globalCompositeOperation = "source-over"; resolve(canvas.toDataURL("image/png")); }; - maskImg.src = this.confirmAsset(args.MASK, "png"); + maskImg.src = this.convertAsset(args.MASK, "png"); }; - srcImg.src = this.confirmAsset(args.IMG, "png"); + srcImg.src = this.convertAsset(args.IMG, "png"); }); } applyAbberationEffect(args) { return new Promise((resolve) => { - const amtIn = Scratch.Cast.toNumber(args.PERCENTAGE); + const amtIn = cast.toNumber(args.PERCENTAGE); const img = new Image(); img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width + Math.abs(amtIn) * 5, img.height + Math.abs(amtIn) * 5, img, Math.abs(amtIn) * 2.5, Math.abs(amtIn) * 2.5); @@ -945,13 +972,11 @@ ctx.putImageData(imageData, 0, 0); resolve(canvas.toDataURL()); }; - img.src = this.confirmAsset(args.SVG, "png"); + img.src = this.convertAsset(args.SVG, "png"); }); } applyChromAb(imageData, color1, color2, amtIn, dir) { - const data = imageData.data; - let width = imageData.width; - let height = imageData.height; + let { data, width, height } = imageData; const copy1 = new Uint8ClampedArray(data.length); const copy2 = new Uint8ClampedArray(data.length); const rgb1 = hexToRgb(color1); @@ -999,21 +1024,20 @@ }); } svgToBitmap(args) { - return this.stretch(this.confirmAsset(args.SVG, "png"), Math.abs(Scratch.Cast.toNumber(args.WIDTH)), Math.abs(Scratch.Cast.toNumber(args.HEIGHT))); + return this.stretch(this.convertAsset(args.SVG, "png"), Math.abs(cast.toNumber(args.WIDTH)), Math.abs(cast.toNumber(args.HEIGHT))); } stretchImg(args) { - return this.stretch(this.confirmAsset(args.URI, "png"), Math.abs(Scratch.Cast.toNumber(args.W)), Math.abs(Scratch.Cast.toNumber(args.H))); + return this.stretch(this.convertAsset(args.URI, "png"), Math.abs(cast.toNumber(args.W)), Math.abs(cast.toNumber(args.H))); } convertImageToSVG(args) { return new Promise((resolve) => { const img = new Image(); - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); img.onload = () => { const ctx = this.createCanvasCtx(img.width, img.height, img).ctx; ctx.drawImage(img, 0, 0, img.width, img.height); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("version", "1.1"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); svg.setAttribute("width", img.width.toFixed(5)); @@ -1057,16 +1081,15 @@ // eslint-disable-next-line const img = new Image(); img.onload = () => { - const width = img.width; - const height = img.height; - const svg = ` `; resolve(args.TYPE === "dataURI" ? `data:image/svg+xml;base64,${btoa(svg)}` : svg); }; - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); }); } else { return args.URI } } @@ -1078,12 +1101,12 @@ const pixelData = this.printImg(img); const ctx = this.createCanvasCtx(img.width, img.height).ctx; ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), img.width, img.height), 0, 0); - const factor = Scratch.Cast.toNumber(args.NUM) / 10; + const factor = cast.toNumber(args.NUM) / 10; const weights = [0, -factor, 0, -factor, 1 + 4 * factor, -factor, 0, -factor, 0]; this.sharpen(ctx, img.width, img.height, weights, 25); resolve(this.exportImg(img, ctx.getImageData(0, 0, img.width, img.height).data)); }; - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); }); } sharpen(ctx, width, height, weights, alphaThreshold) { @@ -1120,8 +1143,8 @@ audioToImage(args) { const audioURI = args.AUDIO_URI; - const imageWidth = Math.abs(Scratch.Cast.toString(args.W)); - const { canvas, ctx } = this.createCanvasCtx(imageWidth, Math.abs(Scratch.Cast.toString(args.H))); + const imageWidth = Math.abs(cast.toString(args.W)); + const { canvas, ctx } = this.createCanvasCtx(imageWidth, Math.abs(cast.toString(args.H))); for (let i = 0; i < audioURI.length; i++) { const charCode = audioURI.charCodeAt(i); ctx.fillStyle = `rgb(${(charCode * 2) % 256},${(charCode * 3) % 256},${(charCode * 4) % 256})`; @@ -1158,8 +1181,8 @@ if (translateMatch) translateValues = [parseFloat(translateMatch[1]), parseFloat(translateMatch[2])]; values = `${viewBoxValues},${translateValues}`; values = values.split(","); - values = values.map(item => Scratch.Cast.toNumber(item)); - amt = Scratch.Cast.toNumber(amt); + values = values.map(item => cast.toNumber(item)); + amt = cast.toNumber(amt); if (values.length > 3) { svg = svg.replace(/viewBox="([^"]+)"/, `viewBox="${values[0]},${values[1]},${values[2] + (amt * 2)},${values[3] + (amt * 2)}"`); svg = svg.replace(/width="([^"]+)"/, `width="${values[2] + (amt * 2)}"`); @@ -1169,7 +1192,30 @@ return svg; } - removeThorns(args) { return args.SVG.replaceAll("linejoin=\"miter\"", "linejoin=\"round\"") } + removeThorns(args) { + return cast.toString(args.SVG).replaceAll("linejoin=\"miter\"", "linejoin=\"round\""); + } + + commonCol(args) { + const img = new Image(); + img.src = this.convertAsset(args.URI, "png"); + return new Promise((resolve) => { + img.onload = () => { + const data = this.printImg(img); + const colorCnt = {}; + for (let i = 0; i < data.length; i += 4) { + if (data[i + 3] === 0) continue; + const key = `${data[i]},${data[i + 1]},${data[i + 2]},${data[i + 3]}`; + colorCnt[key] = (colorCnt[key] || 0) + 1; + } + let rgb = null; + if (args.TYPE === "most") rgb = Object.keys(colorCnt).reduce((a, b) => colorCnt[a] > colorCnt[b] ? a : b); + else rgb = Object.keys(colorCnt).reduce((a, b) => colorCnt[a] < colorCnt[b] ? a : b); + rgb = rgb.split(",").map(Number); + resolve(rgbaToHex(...rgb)); + }; + }); + } numPixels(args) { const img = new Image(); @@ -1184,26 +1230,27 @@ setPixel(args) { return this.setPixels(args) } setPixels(args) { const img = new Image(); - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); return new Promise((resolve) => { img.onload = () => { - const startNum = Scratch.Cast.toNumber(args.NUM); - const endNum = Scratch.Cast.toNumber(args.NUM2) || startNum; + const startNum = cast.toNumber(args.NUM); + const endNum = cast.toNumber(args.NUM2) || startNum; const pixelData = this.printImg(img); for (let num = startNum; num <= endNum && num <= img.width * img.height; num++) { const rgb = hexToRgb(args.COLOR); - for (let i = 0; i < 4; i++) { pixelData[((num - 1) * 4) + i] = rgb[i] } + for (let i = 0; i < 4; i++) pixelData[((num - 1) * 4) + i] = rgb[i]; } resolve(this.exportImg(img, pixelData)); }; }); } + getPixel(args) { const img = new Image(); - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); return new Promise((resolve) => { img.onload = () => { - const targetPixel = Scratch.Cast.toNumber(args.NUM); + const targetPixel = cast.toNumber(args.NUM); const pixelData = this.printImg(img); if (targetPixel >= 1 && targetPixel <= img.width * img.height) { const pixelIndex = (targetPixel - 1) * 4; @@ -1217,7 +1264,7 @@ crackImage(args) { const cracks = Math.max(2, args.SHARDS); const img = new Image(); - img.src = this.confirmAsset(args.URI, "png"); + img.src = this.convertAsset(args.URI, "png"); const newWidth = img.width * 4; const newHeight = img.height * 4; this.allShards = []; @@ -1250,6 +1297,11 @@ } getShard(args) { return this.allShards[args.SHARD - 1] || "" } + + // Deprecated + convertHexToRGB(args) { return hexToRgb(args.HEX)[{ R: 0, G: 1, B: 2 }[args.CHANNEL]] || "" } + clipImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} + overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From 6a01847ccaac8868238671b97357519e80b59962 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:12:07 -0700 Subject: [PATCH 23/26] oh yea remove unused things --- extensions/SharkPool/Image-Effects.js | 30 +-------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index b0934c967c..b6ee3b09c0 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -393,28 +393,6 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } - }, - // Deprecated - { - opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, - text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, - arguments: { - HEX: { type: Scratch.ArgumentType.COLOR }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } - } } ], menus: { @@ -424,8 +402,7 @@ REMOVAL: ["under", "over", "equal to"], DOMINANT: ["most", "least"], fileType: ["content", "dataURI"], - EFFECTS: { acceptReporters: true, items: imageEffectMenu }, - CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated + EFFECTS: { acceptReporters: true, items: imageEffectMenu } }, }; } @@ -1297,11 +1274,6 @@ } getShard(args) { return this.allShards[args.SHARD - 1] || "" } - - // Deprecated - convertHexToRGB(args) { return hexToRgb(args.HEX)[{ R: 0, G: 1, B: 2 }[args.CHANNEL]] || "" } - clipImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} - overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From 9ea08e6108ee035806d17875e4e8dc17b69947ad Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:33:39 -0700 Subject: [PATCH 24/26] Image-Effects -- Faster Processing --- extensions/SharkPool/Image-Effects.js | 118 +++++++++++++++----------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index b6ee3b09c0..425ddd754a 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -2,8 +2,9 @@ // ID: imgEffectsSP // Description: Apply a variety of new effects to the data URI of Images or Costumes. // By: SharkPool +// Licence: MIT -// Version V.2.4.1 +// Version V.2.5.0 (function (Scratch) { "use strict"; @@ -24,8 +25,8 @@ return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}${alpha}`; }; const imageEffectMenu = [ - "Saturation", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", "Vignette", - "Ripple", "Displacement", "Posterize", "Blur", "Scanlines", "Grain", "Cubism", + "Saturation", "Contrast", "Opaque", "Glitch", "Chunk Glitch", "Clip Glitch", "Vignette", + "Ripple", "Displacement", "Posterize", "Blur", "Sepia", "Scanlines", "Grain", "Cubism", ]; class imgEffectsSP { @@ -393,6 +394,28 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } + }, + // Deprecated + { + opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, + text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, + arguments: { + MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } + } + }, + { + opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, + text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, + arguments: { + HEX: { type: Scratch.ArgumentType.COLOR }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } + } } ], menus: { @@ -402,7 +425,8 @@ REMOVAL: ["under", "over", "equal to"], DOMINANT: ["most", "least"], fileType: ["content", "dataURI"], - EFFECTS: { acceptReporters: true, items: imageEffectMenu } + EFFECTS: { acceptReporters: true, items: imageEffectMenu }, + CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated }, }; } @@ -501,21 +525,26 @@ const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const effectFunc = this[`apply${cast.toString(args.EFFECT).replaceAll(" ", "")}`]; - if (effectFunc && typeof effectFunc === "function") await effectFunc(imageData, percent); + if (effectFunc && typeof effectFunc === "function") await effectFunc(imageData, percent, ctx); else resolve(""); - ctx.putImageData(imageData, 0, 0); + if (imageData.isAltered === undefined) ctx.putImageData(imageData, 0, 0); + else { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, img.width, img.height); + if (imageData.extraDraw) imageData.extraDraw(ctx); + } resolve(canvas.toDataURL()); }; img.src = this.convertAsset(args.SVG, "png"); }); } - applySaturation(imageData, amtIn) { - const data = imageData.data; - amtIn /= 100; - for (let i = 0; i < data.length; i += 4) { - const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; - for (let j = 0; j < 3; j++) { data[i + j] = avg + (data[i + j] - avg) * amtIn } - } + applySaturation(imageData, amtIn, ctx) { + ctx.filter = `saturate(${Math.abs(amtIn)}%)${amtIn < 0 ? " invert(100%)" : ""}`; + imageData.isAltered = true; + } + applyContrast(imageData, amtIn, ctx) { + ctx.filter = `contrast(${Math.max(0, amtIn / 100) + 1})`; + imageData.isAltered = true; } applyOpaque(imageData, amtIn) { const data = imageData.data; @@ -568,20 +597,21 @@ } } } - applyVignette(imageData, amtIn) { - const { data, width, height} = imageData; - let center = [width / 2, height / 2]; - const maxDistance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const index = (y * width + x) * 4; - center = [Math.abs(x - center[0]), Math.abs(y - center[1])]; - const distance = Math.sqrt(center[0] * center[0] + center[1] * center[1]); - let vigAMT = (amtIn < 0) ? 1 - (distance / maxDistance) * (amtIn / 100) : ((maxDistance - distance) / maxDistance) * (amtIn / 100); - vigAMT = Math.max(0, Math.min(1, vigAMT)); - for (let i = 0; i < 3; i++) data[index + i] = Math.round(data[index + i] * vigAMT); - } - } + applyVignette(imageData, amtIn, ctx) { + const { width, height} = imageData; + const col = amtIn > 0 ? 255 : 0; + amtIn = Math.abs(amtIn) / 100; + const grad = ctx.createRadialGradient( + width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 1.5 + ); + grad.addColorStop(0, `rgba(${col}, ${col}, ${col}, 0)`); + grad.addColorStop(1, `rgba(${col}, ${col}, ${col}, ${amtIn})`); + imageData.isAltered = true; + imageData.extraDraw = (ctx) => { + ctx.globalCompositeOperation = "source-atop"; + ctx.fillStyle = grad; + ctx.fillRect(0, 0, width, height); + }; } applyRipple(imageData, amtIn) { const { data, width, height} = imageData; @@ -627,28 +657,13 @@ } } } - applyBlur(imageData, amtIn) { - const { data, width, height} = imageData; - const radius = Math.floor((amtIn / 100) * 10); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - let sum = [0, 0, 0, 0]; - let count = 0; - for (let ky = -radius; ky <= radius; ky++) { - for (let kx = -radius; kx <= radius; kx++) { - const offsetX = x + kx; - const offsetY = y + ky; - if (offsetX >= 0 && offsetX < width && offsetY >= 0 && offsetY < height) { - const pixelX = (offsetY * width + offsetX) * 4; - for (let i = 0; i < 4; i++) sum[i] += data[pixelX + i]; - count++; - } - } - } - const pixelY = (y * width + x) * 4; - if (count > 0) for (let i = 0; i < 4; i++) data[pixelY + i] = sum[i] / count; - } - } + applyBlur(imageData, amtIn, ctx) { + ctx.filter = `blur(${amtIn}px)`; + imageData.isAltered = true; + } + applySepia(imageData, amtIn, ctx) { + ctx.filter = `sepia(${amtIn}%)`; + imageData.isAltered = true; } applyScanlines(imageData, amtIn) { const { data, width, height} = imageData; @@ -1274,6 +1289,11 @@ } getShard(args) { return this.allShards[args.SHARD - 1] || "" } + + // Deprecated + convertHexToRGB(args) { return hexToRgb(args.HEX)[{ R: 0, G: 1, B: 2 }[args.CHANNEL]] || "" } + clipImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} + overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From 455ff2a653f809fc43f88d4e727dc94906d165f4 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:35:34 -0700 Subject: [PATCH 25/26] remove deprecated stuff --- extensions/SharkPool/Image-Effects.js | 30 +-------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 425ddd754a..12d03bfd02 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -394,28 +394,6 @@ arguments: { SVG: { type: Scratch.ArgumentType.STRING, defaultValue: "" } } - }, - // Deprecated - { - opcode: "clipImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] from [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "overlayImage", blockType: Scratch.BlockType.REPORTER, - text: "clip [CUTOUT] onto [MAIN]", hideFromPalette: true, - arguments: { - MAIN: { type: Scratch.ArgumentType.STRING, defaultValue: "source-here" }, CUTOUT: { type: Scratch.ArgumentType.STRING, defaultValue: "cutout-here" } - } - }, - { - opcode: "convertHexToRGB", blockType: Scratch.BlockType.REPORTER, - text: "convert [HEX] to [CHANNEL]", hideFromPalette: true, - arguments: { - HEX: { type: Scratch.ArgumentType.COLOR }, CHANNEL: { type: Scratch.ArgumentType.STRING, menu: "CHANNELS" } - } } ], menus: { @@ -425,8 +403,7 @@ REMOVAL: ["under", "over", "equal to"], DOMINANT: ["most", "least"], fileType: ["content", "dataURI"], - EFFECTS: { acceptReporters: true, items: imageEffectMenu }, - CHANNELS: { acceptReporters: true, items: ["R", "G", "B"] } // Deprecated + EFFECTS: { acceptReporters: true, items: imageEffectMenu } }, }; } @@ -1289,11 +1266,6 @@ } getShard(args) { return this.allShards[args.SHARD - 1] || "" } - - // Deprecated - convertHexToRGB(args) { return hexToRgb(args.HEX)[{ R: 0, G: 1, B: 2 }[args.CHANNEL]] || "" } - clipImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=s.width,t.height=s.height;let a=t.getContext("2d"),h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.drawImage(s,0,0),a.globalCompositeOperation="destination-in";let l=(this.cutoutDirection+270)*Math.PI/180;a.translate(n+h/2,-1*r+o/2),a.rotate(l),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),a.globalCompositeOperation="source-over",e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} - overlayImage(t){return new Promise((e,i)=>{let s=new Image;s.onload=()=>{let i=new Image;i.onload=()=>{let t=document.createElement("canvas");t.width=Math.max(s.width,i.width),t.height=Math.max(s.height,i.height);let a=t.getContext("2d");a.drawImage(s,0,0);let h=i.width+this.scale[0],o=i.height+this.scale[1],n=this.cutPos[0]+s.width/2-h/2,r=this.cutPos[1]-s.height/2+o/2;a.translate(n+h/2,-1*r+o/2),a.rotate((this.cutoutDirection+270)*Math.PI/180),a.drawImage(i,-h/2,-o/2,h,o),a.setTransform(1,0,0,1,0,0),e(t.toDataURL("image/png"))},i.src=this.convertAsset(t.CUTOUT,"png")},s.src=this.convertAsset(t.MAIN,"png")})} } Scratch.extensions.register(new imgEffectsSP()); From 1dc1aaccfe2bc5a30c572cb59f3f3db7199ac0d5 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:38:22 -0800 Subject: [PATCH 26/26] Image-Effects -- Allow Negative Stretching and URLs --- extensions/SharkPool/Image-Effects.js | 57 +++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/extensions/SharkPool/Image-Effects.js b/extensions/SharkPool/Image-Effects.js index 12d03bfd02..5ecc029f41 100644 --- a/extensions/SharkPool/Image-Effects.js +++ b/extensions/SharkPool/Image-Effects.js @@ -4,7 +4,7 @@ // By: SharkPool // Licence: MIT -// Version V.2.5.0 +// Version V.2.5.01 (function (Scratch) { "use strict"; @@ -411,14 +411,17 @@ // Helper Funcs clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } - printImg(img, forceWid, forceHei) { - const { canvas, ctx } = this.createCanvasCtx(forceWid || img.width, forceHei || img.height); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + printImg(img, width, height) { + const { canvas, ctx } = this.createCanvasCtx(Math.abs(width) || img.width, Math.abs(height) || img.height); + ctx.save(); + ctx.scale(width < 0 ? -1 : 1, height < 0 ? -1 : 1); + ctx.drawImage(img, width < 0 ? -Math.abs(width) : 0, height < 0 ? -Math.abs(height) : 0, canvas.width, canvas.height); + ctx.restore(); return ctx.getImageData(0, 0, canvas.width, canvas.height).data; } - exportImg(img, pixelData, forceWid, forceHei) { - const { canvas, ctx } = this.createCanvasCtx(forceWid || img.width, forceHei || img.height); + exportImg(img, pixelData, width, height) { + const { canvas, ctx } = this.createCanvasCtx(Math.abs(width) || img.width, Math.abs(height) || img.height); ctx.putImageData(new ImageData(new Uint8ClampedArray(pixelData), canvas.width, canvas.height), 0, 0); return canvas.toDataURL(); } @@ -448,9 +451,11 @@ } convertAsset(input, type) { - if (!input || !(input.startsWith("data:image/") || input.startsWith(" { const color = hexToRgb(args.COLOR); const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const data = this.printImg(img); for (let i = 0; i < data.length; i += 4) { @@ -480,17 +486,18 @@ const colRem = hexToRgb(args.COLOR); const colRep = hexToRgb(args.REPLACE); return new Promise(resolve => { - const imageElement = new Image(); - imageElement.onload = () => { - const pixelData = this.printImg(imageElement); + const img = new Image(); + img.crossOrigin = "Anonymous"; + img.onload = () => { + const pixelData = this.printImg(img); for (let i = 0; i < pixelData.length; i += 4) { const [r, g, b] = pixelData.slice(i, i + 3); const inRange = (val, target) => val >= target - this.softness && val <= target + this.softness; if (inRange(r, colRem[0]) && inRange(g, colRem[1]) && inRange(b, colRem[2])) pixelData.set(colRep, i); } - resolve(this.exportImg(imageElement, pixelData)); + resolve(this.exportImg(img, pixelData)); }; - imageElement.src = this.convertAsset(args.DATA_URI, "png"); + img.src = this.convertAsset(args.DATA_URI, "png"); }); } @@ -498,6 +505,7 @@ return new Promise((resolve) => { const percent = cast.toNumber(args.PERCENTAGE); const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = async () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -706,6 +714,7 @@ return new Promise((resolve) => { const strength = cast.toNumber(args.STRENGTH) / 100; const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const canvasSize = Math.max(img.width, img.height) * 2; let centerX = cast.toNumber(args.CENTER_X) / 100; @@ -750,6 +759,7 @@ const freqX = cast.toNumber(args.FREQX) / 100; const freqY = cast.toNumber(args.FREQY) / 100; const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -781,6 +791,7 @@ return new Promise((resolve) => { const threshold = cast.toNumber(args.THRESHOLD) / 100; const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -810,6 +821,7 @@ const amtIn = cast.toNumber(args.PERCENTAGE) / 100; const width = cast.toNumber(args.WIDTH) / 50; const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width, img.height, img, 0, 0); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -845,6 +857,7 @@ const thick = Math.ceil(cast.toNumber(args.THICKNESS) / 4); const color = hexToRgb(args.COLOR); const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width + (thick * 2), img.height + (thick * 2), img, thick, thick); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -906,8 +919,10 @@ maskImage(args) { return new Promise((resolve) => { const srcImg = new Image(); + srcImg.crossOrigin = "Anonymous"; srcImg.onload = () => { const maskImg = new Image(); + maskImg.crossOrigin = "Anonymous"; maskImg.onload = () => { const scaleW = maskImg.width * (this.scale[0] / 50); const scaleH = maskImg.height * (this.scale[1] / 50); @@ -934,6 +949,7 @@ return new Promise((resolve) => { const amtIn = cast.toNumber(args.PERCENTAGE); const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { canvas, ctx } = this.createCanvasCtx(img.width + Math.abs(amtIn) * 5, img.height + Math.abs(amtIn) * 5, img, Math.abs(amtIn) * 2.5, Math.abs(amtIn) * 2.5); let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -986,6 +1002,7 @@ stretch(src, w, h) { return new Promise((resolve) => { const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { resolve(this.exportImg(img, this.printImg(img, w, h), w, h)); }; @@ -993,16 +1010,17 @@ }); } svgToBitmap(args) { - return this.stretch(this.convertAsset(args.SVG, "png"), Math.abs(cast.toNumber(args.WIDTH)), Math.abs(cast.toNumber(args.HEIGHT))); + return this.stretch(this.convertAsset(args.SVG, "png"),cast.toNumber(args.WIDTH), cast.toNumber(args.HEIGHT)); } stretchImg(args) { - return this.stretch(this.convertAsset(args.URI, "png"), Math.abs(cast.toNumber(args.W)), Math.abs(cast.toNumber(args.H))); + return this.stretch(this.convertAsset(args.URI, "png"), cast.toNumber(args.W), cast.toNumber(args.H)); } convertImageToSVG(args) { return new Promise((resolve) => { const img = new Image(); img.src = this.convertAsset(args.URI, "png"); + img.crossOrigin = "Anonymous"; img.onload = () => { const ctx = this.createCanvasCtx(img.width, img.height, img).ctx; ctx.drawImage(img, 0, 0, img.width, img.height); @@ -1049,6 +1067,7 @@ return await new Promise((resolve) => { // eslint-disable-next-line const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const { width, height } = img; const svg = ` { const img = new Image(); + img.crossOrigin = "Anonymous"; img.onload = () => { const pixelData = this.printImg(img); const ctx = this.createCanvasCtx(img.width, img.height).ctx; @@ -1169,6 +1189,7 @@ const img = new Image(); img.src = this.convertAsset(args.URI, "png"); return new Promise((resolve) => { + img.crossOrigin = "Anonymous"; img.onload = () => { const data = this.printImg(img); const colorCnt = {}; @@ -1190,6 +1211,7 @@ const img = new Image(); img.src = this.confirmAsset(args.URI, "png"); return new Promise((resolve) => { + img.crossOrigin = "Anonymous"; img.onload = () => { resolve(args.TYPE === "total" ? img.width * img.height : args.TYPE === "per line" ? img.width : img.height); }; @@ -1201,6 +1223,7 @@ const img = new Image(); img.src = this.convertAsset(args.URI, "png"); return new Promise((resolve) => { + img.crossOrigin = "Anonymous"; img.onload = () => { const startNum = cast.toNumber(args.NUM); const endNum = cast.toNumber(args.NUM2) || startNum; @@ -1218,6 +1241,7 @@ const img = new Image(); img.src = this.convertAsset(args.URI, "png"); return new Promise((resolve) => { + img.crossOrigin = "Anonymous"; img.onload = () => { const targetPixel = cast.toNumber(args.NUM); const pixelData = this.printImg(img); @@ -1238,6 +1262,7 @@ const newHeight = img.height * 4; this.allShards = []; return new Promise((resolve) => { + img.crossOrigin = "Anonymous"; img.onload = () => { for (let i = 0; i < cracks; i++) { if (this.allShards.length >= args.SHARDS) break;