diff --git a/src/lib/extensions.js b/src/lib/extensions.js index 9567327f..cc6fa89d 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -19,6 +19,13 @@ export default [ banner: "pooiod/B2Dimg.svg", creator: "pooiod7", }, + { + name: "Rect", // The name of the extension. + description: "Creates a Rectangle-Based object which you can grab positions of (Works with Vectors!)", + code: "IrshaadAli/static_Rect.js", // There exists a translated version too + banner: "IrshaadAli/Rect.svg", // Sorry - Don't know how to avif + creator: "Irshaad_Ali", // Had to create a new one + }, { name: "3D Math", description: "A handful of utilities for making your own sprite-based 3D engine.", diff --git a/static/extensions/IrshaadAli/static_Rect.js b/static/extensions/IrshaadAli/static_Rect.js new file mode 100644 index 00000000..b138e15f --- /dev/null +++ b/static/extensions/IrshaadAli/static_Rect.js @@ -0,0 +1,632 @@ +(function (Scratch) { + const Cast = Scratch.Cast; + const BlockType = Scratch.BlockType; + const BlockShape = Scratch.BlockShape; + const ArgumentType = Scratch.ArgumentType; + + const vm = Scratch.vm; + const mouse = vm.runtime.ioDevices.mouse + + if (!vm.jwVector) vm.extensionManager.loadExtensionIdSync('jwVector') + + const jwVector = vm.jwVector + const Vector = jwVector.Type + + /** + * @param {number} x + * @returns {string} + */ + function formatNumber(x) { + if (x >= 1e6) { + return x.toExponential(4) + } else { + x = Math.floor(x * 1000) / 1000 + return x.toFixed(Math.min(3, (String(x).split('.')[1] || '').length)) + } + } + + function span(text) { + let el = document.createElement('span') + el.innerHTML = text + el.style.display = 'hidden' + el.style.whiteSpace = 'nowrap' + el.style.width = '100%' + el.style.textAlign = 'center' + return el + } + + + class RectType { + customId = "IAliRect" + + constructor(x = 0, y = 0, width = 0, height = 0) { + this.x = isNaN(x) ? 0 : x + this.y = isNaN(y) ? 0 : y + this.width = isNaN(width) ? 0 : width + this.height = isNaN(height) ? 0 : height + } + + static toRect(r) { + if (r instanceof RectType) return r + if (r instanceof Array && r.length == 4) return new RectType(r[0], r[1], r[2], r[3]) + if (r instanceof Array && r.length == 2) { + let x = 0 + let y = 0 + let w = 0 + let h = 0 + + if (r[0] instanceof Vector) { + r[0] = Vector.toVector(r[0]) + x = r[0].x + y = r[0].y + } else { + x = r[0][0] + y = r[0][1] + } + if (r[1] instanceof Vector) { + r[1] = Vector.toVector(r[1]) + w = r[1].x + h = r[1].y + } else { + w = r[1][0] + h = r[1][1] + } + + return new RectType(x, y, w, h) + } + if (String(r).split(',')) { + let array = String(r).split(',').map(value => Cast.toNumber(value)) + return new RectType( + array[0], + array[1], + array[2], + array[3], + ) + } + return new RectType(0, 0, 0, 0) + } + + IAliRectHandler() { + return "Rect" + } + + toString() { + return `${this.x},${this.y},${this.width},${this.height}` + } + + toMonitorContent = () => span(this.toString()) + + toReporterContent() { + let root = document.createElement('div') + root.style.display = 'flex' + root.style.width = "200px" + root.style.overflow = "hidden" + + let details = document.createElement('div') + details.style.display = 'flex' + details.style.flexDirection = 'column' + details.style.justifyContent = 'center' + details.style.width = "100px" + + details.appendChild(span(`X: ${formatNumber(this.x)}`)) + details.appendChild(span(`Y: ${formatNumber(this.y)}`)) + details.appendChild(span(`W: ${formatNumber(this.width)}`)) + details.appendChild(span(`H: ${formatNumber(this.height)}`)) + + root.appendChild(details) + + let square = document.createElement("div") + square.style.width = "84px" + square.style.height = "84px" + square.style.margin = "8px" + square.style.border = "4px solid black" + square.style.boxSizing = "border-box" + + root.append(square) + return root + } + + get offsets() { + return { + 'left': this.width / -2.0, + 'top': this.height / 2.0, + 'right': this.width / 2.0, + 'bottom': this.height / -2.0, + 'center': 0 + } + } + + getPoint(type) { + switch (type) { + case 'x': + return this.x + case 'y': + return this.y + case 'width': + return this.width + case 'height': + return this.height + + case 'left': + return this.x + this.offsets.left + case 'top': + return this.y + this.offsets.top + case 'bottom': + return this.y + this.offsets.bottom + case 'right': + return this.x + this.offsets.right + + case 'topleft x': + return this.getPoint('left') + case 'topleft y': + return this.getPoint('top') + + case 'topleft': + return new Vector(this.getPoint('left'), this.getPoint('top')) + case 'midtop': + return new Vector(this.x, this.getPoint('top')) + case 'topright': + return new Vector(this.getPoint('right'), this.getPoint('top')) + + case 'midleft': + return new Vector(this.getPoint('left'), this.y) + case 'center': + return new Vector(this.x, this.y) + case 'midright': + return new Vector(this.getPoint('right'), this.y) + + case 'bottomleft': + return new Vector(this.getPoint('left'), this.getPoint('bottom')) + case 'midbottom': + return new Vector(this.x, this.getPoint('bottom')) + case 'bottomright': + return new Vector(this.getPoint('right'), this.getPoint('bottom')) + case 'size': + return new Vector(this.width, this.height) + } + return NaN + } + + setSinglePoint(type, value) { + value = Cast.toNumber(value) + switch (type) { + case 'x': this.x = value; break; + case 'y': this.y = value; break; + case 'width': this.width = value; break; + case 'height': this.height = value; break; + + case 'topleft x': this.x = value - this.offsets.left; break; + case 'topleft y': this.y = value - this.offsets.top; break; + } + } + + setVectorPoint(type, value) { + value = Vector.toVector(value) + switch (type) { + case 'topleft': + this.x = value.x - this.offsets.left; + this.y = value.y - this.offsets.top; + break; + case 'midtop': + this.x = value.x; + this.y = value.y - this.offsets.top; + break; + case 'topright': + this.x = value.x - this.offsets.right; + this.y = value.y - this.offsets.left; + break; + case 'midleft': + this.x = value.x - this.offsets.left; + this.y = value.y; + break; + case 'center': + this.x = value.x + this.y = value.y + break; + case 'midright': + this.x = value.x - this.offsets.right; + this.y = value.y; + break; + case 'bottomleft': + this.x = value.x - this.offsets.left; + this.y = value.y - this.offsets.bottom; + break; + case 'midbottom': + this.x = value.x; + this.y = value.y - this.offsets.bottom; + break; + case 'bottomright': + this.x = value.x - this.offsets.right; + this.y = value.y - this.offsets.bottom; + break; + + case 'size': + this.width = value.x; + this.height = value.y; + break; + } + } + + collidesXYPoint(x, y) { + x = isNaN(x) ? 0 : x + y = isNaN(y) ? 0 : y + + return ( + x >= this.getPoint('left') && x <= this.getPoint('right') && + y >= this.getPoint('bottom') && y <= this.getPoint('top') + ) + } + + collidesVectorPoint(vec) { + vec = Vector.toVector(vec) + + return ( + vec.x >= this.getPoint('left') && vec.x <= this.getPoint('right') && + vec.y >= this.getPoint('bottom') && vec.y <= this.getPoint('top') + ) + } + + collidesRect(rect) { + rect = RectType.toRect(rect) + + return ( + this.getPoint('left') <= rect.getPoint('right') && + this.getPoint('right') >= rect.getPoint('left') && + this.getPoint('top') >= rect.getPoint('bottom') && + this.getPoint('bottom') <= rect.getPoint('top') + ) + } + } + + const Rect = { + Type: RectType, + Block: { + blockType: BlockType.REPORTER, + blockShape: BlockShape.SQUARE, + // forceOutputType: "Rect", + disableMonitor: true, + allowDropAnywhere: true, + }, + Argument: { + shape: BlockShape.SQUARE, + }, + + NumberArg: { + type: ArgumentType.NUMBER, + defaultValue: 0, + }, + + SinglePointArg: { + menu: 'singlePoint', + defaultValue: 'x' + }, + + VectorPointArg: { + menu: 'vectorPoint', + defaultValue: 'topleft', + } + } + + class Extension { + constructor () { + vm.IAliRect = Rect + vm.runtime.registerSerializer( + "IAliRect", + v => [v.x, v.y, v.width, v.height], + v => new RectType(v.x, v.y, v.width, v.height) + ) + } + + getInfo() { + let blocks = [ + { + opcode: 'fromSprite', + text: 'from [SPRITE]', + arguments: { + SPRITE: { + type: ArgumentType.STRING, + menu: 'sprites' + } + }, + ...Rect.Block + }, + '---', + { + opcode: 'newRect4', + text: 'rect x: [X] y: [Y] w: [W] h: [H]', + arguments: { + X: Rect.NumberArg, + Y: Rect.NumberArg, + W: Rect.NumberArg, + H: Rect.NumberArg, + }, + ...Rect.Block + }, + { + opcode: 'newRect2', + text: 'rect xy: [XY] wh: [WH]', + arguments: { + XY: jwVector.Argument, + WH: jwVector.Argument, + }, + ...Rect.Block + }, + ] + + blocks = blocks.concat([{ + opcode: 'newRect1', + text: 'rect xywh: [XYWH]', + hideFromPalette: !vm.runtime.ext_jwArray, + arguments: { + XYWH: vm.runtime.ext_jwArray ? vm.jwArray.Argument : ArgumentType.CUSTOM, + }, + ...Rect.Block + }]) + + blocks = blocks.concat([ + '---', + { + opcode: 'getSinglePoint', + text: 'get [RECT] [TYPE]', + blockType: BlockType.REPORTER, + arguments: { + RECT: Rect.Argument, + TYPE: Rect.SinglePointArg + } + }, + { + opcode: 'setSinglePoint', + text: 'set [RECT] [TYPE] to [VALUE]', + arguments: { + RECT: Rect.Argument, + TYPE: Rect.SinglePointArg, + VALUE: Rect.NumberArg, + }, + ...Rect.Block + }, + + '---', + + { + opcode: 'getVectorPoint', + text: 'get [RECT] [TYPE]', + arguments: { + RECT: Rect.Argument, + TYPE: Rect.VectorPointArg + }, + ...jwVector.Block + }, + { + opcode: 'setVectorPoint', + text: 'set [VALUE] [TYPE] to [VALUE]', + arguments: { + RECT: Rect.Argument, + TYPE: Rect.VectorPointArg, + VALUE: jwVector.Argument + }, + ...Rect.Block + }, + + { + blockType: BlockType.LABEL, + text: 'Collisions' + }, + + { + opcode: 'collidingXY', + text: '[RECT] colliding with x: [X] y: [Y]?', + blockType: BlockType.BOOLEAN, + arguments: { + RECT: Rect.Argument, + X: Rect.NumberArg, + Y: Rect.NumberArg, + } + }, + + { + opcode: 'collidingPoint', + text: '[RECT] colliding with point [VECTOR]?', + blockType: BlockType.BOOLEAN, + arguments: { + RECT: Rect.Argument, + VECTOR: jwVector.Argument, + } + }, + + { + opcode: 'collidingRect', + text: '[RECTA] colliding with rect [RECTB]?', + blockType: Scratch.BlockType.BOOLEAN, + arguments: { + RECTA: Rect.Argument, + RECTB: Rect.Argument, + }, + }, + + { + opcode: 'collidingMouse', + text: '[RECT] touching mouse (via client [CLIENT] and centered [CENTERED])?', + blockType: BlockType.BOOLEAN, + arguments: { + RECT: Rect.Argument, + CLIENT: {type: ArgumentType.BOOLEAN, shape: BlockShape.HEXAGONAL}, + CENTERED: {type: ArgumentType.BOOLEAN, shape: BlockShape.HEXAGONAL}, + } + }, + + { + blockType: BlockType.LABEL, + text: 'Extras' + }, + + { + opcode: 'mousePos', + text: 'mouse pos (client: [CLIENT] and centered: [CENTERED])', + arguments: { + CLIENT: { + type: ArgumentType.BOOLEAN, + shape: BlockShape.HEXAGONAL, + }, + CENTERED: {type: ArgumentType.BOOLEAN, shape: BlockShape.HEXAGONAL}, + }, + ...jwVector.Block + }, + + { + opcode: 'screenSize', + text: 'screen size', + ...jwVector.Block + } + ]) + + return { + id: "IAliRect", + name: "Rect", + color1: "#ff0061", + color2: "#d80052", + menuIconURI: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+DQogIDxlbGxpcHNlIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAwLCA5Nyk7IHN0cm9rZTogcmdiKDIxNiwgMCwgODIpOyIgY3g9IjEwIiBjeT0iMTAiIHJ4PSI5LjUiIHJ5PSI5LjUiPjwvZWxsaXBzZT4NCiAgPHJlY3QgeD0iNi4wNTkiIHk9IjUuNzkzIiB3aWR0aD0iOS40NzciIGhlaWdodD0iOS40NzciIHN0eWxlPSJmaWxsOiBub25lOyBzdHJva2U6IHJnYigyNTUsIDI1NSwgMjU1KTsiPjwvcmVjdD4NCiAgPHJlY3QgeD0iNS40OTQiIHk9IjMuMjIyIiB3aWR0aD0iMTAuNjQzIiBoZWlnaHQ9IjEuMzcyIiByeD0iMSIgcnk9IjEiIHN0eWxlPSJmaWxsOiByZ2IoMjU1LCAyNTUsIDI1NSk7Ij48L3JlY3Q+DQogIDxyZWN0IHg9IjEuNjA1IiB5PSI3LjQ3MyIgd2lkdGg9IjEwLjY0MyIgaGVpZ2h0PSIxLjM3MiIgcng9IjEiIHJ5PSIxIiBzdHlsZT0iZmlsbDogcmdiKDI1NSwgMjU1LCAyNTUpOyBzdHJva2Utd2lkdGg6IDE7IHRyYW5zZm9ybS1ib3g6IGZpbGwtYm94OyB0cmFuc2Zvcm0tb3JpZ2luOiA1MCUgNTAlOyIgdHJhbnNmb3JtPSJtYXRyaXgoMCwgMSwgLTEsIDAsIC0yLjcwMTY4NCwgMi41MTc0OCkiPjwvcmVjdD4NCjwvc3ZnPg==", + + blocks: blocks, + menus: { + sprites: { + acceptReporters: true, + items: this.getSpriteMenu() + }, + singlePoint: { + acceptReporters: true, + items: ['x', 'y', 'width', 'height', 'topleft x', 'topleft y'] + }, + vectorPoint: { + acceptReporters: true, + items: [ + 'topleft', 'midtop', 'topright', + 'midleft', 'center', 'midright', + 'bottomleft', 'midbottom', 'bottomright', + 'size' + ], + } + }, + + roundingFunctions: { + acceptReporters: false, + items: [ + { + text: 'round', + value: 'round' + }, + { + text: 'ceil', // might as well go full in on the inconsistencies since we are already doing "round of" + value: 'ceil' + }, + { + text: 'floor', + value: 'floor' + } + ] + } + } + } + + getSpriteMenu(){ + const targets = vm.runtime.targets; + const emptyMenu = [{ text: "", value: "" }]; + if (!targets) return emptyMenu; + const menu = targets.filter(target => target.isOriginal && (!target.isStage)).map(target => ({ text: target.sprite.name, value: target.sprite.name })); + return (menu.length > 0) ? menu : emptyMenu; + } + + fromSprite({SPRITE}) { + let spr = vm.runtime.getSpriteTargetByName(SPRITE) + let x = spr.x + let y = spr.y + + let costume = spr.sprite.costumes_[spr.currentCostume] + let asset = costume.asset + let assetType = asset.assetType.name + let size = costume.size + + return new RectType(x, y, assetType == "ImageBitmap" ? size[0]/2 : size[0], assetType == "ImageBitmap" ? size[1]/2 : size[1]) + } + + + newRect4({X, Y, W, H}) { + return new RectType(X, Y, W, H); + } + + newRect2({XY, WH}) { + XY = Vector.toVector(XY) + WH = Vector.toVector(WH) + return RectType.toRect([XY, WH]); + } + + newRect1({XYWH}) { + return RectType.toRect(XYWH) + } + + getSinglePoint({RECT, TYPE}) { + let val = RectType.toRect(RECT).getPoint(TYPE) + return isNaN(val) ? 0 : val + } + + getVectorPoint({RECT, TYPE}) { + let val = RectType.toRect(RECT).getPoint(TYPE); + return isNaN(val) ? val : new Vector() + } + + setSinglePoint({RECT, TYPE, VALUE}) { + RECT = RectType.toRect(RECT) + RECT.setSinglePoint(TYPE, VALUE); + return RECT; + } + + setVectorPoint({RECT, TYPE, VALUE}) { + RECT = RectType.toRect(RECT) + RECT.setVectorPoint(TYPE, VALUE); + return RECT; + } + + collidingXY({RECT, X, Y}) { + RECT = RectType.toRect(RECT); + X = Cast.toNumber(X); + Y = Cast.toNumber(Y); + + return RECT.collidesXYPoint(X, Y); + } + + collidingPoint({RECT, VECTOR}) { + RECT = RectType.toRect(RECT); + let XY = Vector.toVector(VECTOR); + + return RECT.collidesVectorPoint(XY) + } + + collidingRect({RECTA, RECTB}) { + RECTA = RectType.toRect(RECTA) + RECTB = RectType.toRect(RECTB) + + return RECTA.collidesRect(RECTB) + } + + collidingMouse({RECT, CLIENT, CENTERED}) { + RECT = RectType.toRect(RECT); + let vector = new Vector(CLIENT ? mouse.getClientX() : mouse.getScratchX(), CLIENT ? mouse.getClientY() : mouse.getScratchY()); + vector = CENTERED && CLIENT ? new Vector(vector.x - vm.runtime.stageWidth / 2, -vector.y + vm.runtime.stageHeight / 2) : vector; + return RECT.collidesVectorPoint(vector); + } + + + mousePos({CLIENT, CENTERED}) { + let vector = new Vector(CLIENT ? mouse.getClientX() : mouse.getScratchX(), CLIENT ? mouse.getClientY() : mouse.getScratchY()); + vector = CENTERED && CLIENT ? new Vector(vector.x - vm.runtime.stageWidth / 2, -vector.y + vm.runtime.stageHeight / 2) : vector; + return vector; + } + + screenSize() { + return new Vector(vm.runtime.stageWidth, vm.runtime.stageHeight) + } + + + } + + Scratch.extensions.register(new Extension()) +})(Scratch); diff --git a/static/images/IrshaadAli/Rect.svg b/static/images/IrshaadAli/Rect.svg new file mode 100644 index 00000000..3428eb77 --- /dev/null +++ b/static/images/IrshaadAli/Rect.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/IrshaadAli/RectIcon.svg b/static/images/IrshaadAli/RectIcon.svg new file mode 100644 index 00000000..83516161 --- /dev/null +++ b/static/images/IrshaadAli/RectIcon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file