diff --git a/features/features.json b/features/features.json index 4ef00ca4..32ee58e7 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "rotate-gradient", + "versionAdded": "v4.0.0" + }, { "version": 2, "id": "change-monitor-opacity", diff --git a/features/rotate-gradient/data.json b/features/rotate-gradient/data.json new file mode 100644 index 00000000..899bcaab --- /dev/null +++ b/features/rotate-gradient/data.json @@ -0,0 +1,14 @@ +{ + "title": "Rotate Gradients", + "description": "Allows you to rotate gradients in any direction in the costume editor. Works in both the vector and bitmap editors.", + "credits": [ + { + "url": "https://scratch.mit.edu/users/rgantzos/", + "username": "rgantzos" + } + ], + "type": ["Editor"], + "dynamic": true, + "scripts": [{ "file": "script.js", "runOn": "/projects/*" }], + "styles": [{ "file": "style.css", "runOn": "/projects/*" }] +} diff --git a/features/rotate-gradient/script.js b/features/rotate-gradient/script.js new file mode 100644 index 00000000..71f1a01d --- /dev/null +++ b/features/rotate-gradient/script.js @@ -0,0 +1,203 @@ +export default async function ({ feature, console }) { + let lastRotation = 0 + + feature.page.waitForElements( + "div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']", + function (row) { + let body = document.querySelector(".Popover-body"); + + if (feature.traps.paint().selectedItems.length !== 1) return; + if (feature.traps.paint().selectedItems[0].fillColor._components[0].radial) return; + if (!document.querySelector("div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']")) return; + if (body.querySelector(".ste-direction-slider")) return; + + let div = document.createElement("div"); + div.className = "ste-direction-slider"; + feature.self.hideOnDisable(div); + + let data = document.createElement("div"); + data.className = "color-picker_row-header_173LQ"; + div.appendChild(data); + + let name = document.createElement("span"); + name.className = "color-picker_label-name_17igY"; + name.textContent = "Direction"; + + let value = document.createElement("span"); + value.className = "color-picker_label-readout_9vjb2"; + value.textContent = "0deg"; + + data.appendChild(name); + data.appendChild(value); + + let slider = document.createElement("div"); + slider.className = + "ste-direction-slider-checkered slider_container_o2aIb slider_last_10jvO"; + div.appendChild(slider); + + let sliderBg = document.createElement("div"); + sliderBg.style.background = `linear-gradient(270deg, ${ + feature.traps.paint().selectedItems[0]?.fillColor?._canvasStyle + } 0%, rgba(0, 0, 0, 0) 100%)`; + sliderBg.className = "ste-direction-background"; + slider.appendChild(sliderBg); + + let handle = document.createElement("div"); + handleSlider(handle, value); + handle.className = "ste-direction-handle slider_handle_3f0xk"; + handle.style.left = "124px"; + if (feature.traps.paint().selectedItems[0]?.opacity) { + handle.style.left = "0px"; + } + slider.appendChild(handle); + + body.firstChild.insertBefore(div, body.querySelector("div[class^='color-picker_row-header_']").parentElement); + lastRotation = 0 + } + ); + + feature.redux.subscribe(function() { + if (!document.querySelector("div[class^='paint-editor_editor-container_']")) return; + + if (!document.querySelector("div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']") || feature.traps.paint().selectedItems[0]?.fillColor._components[0].radial || feature.traps.paint().selectedItems.length !== 1) { + document.querySelector(".ste-direction-slider")?.remove() + } +}) + + const rotateColor = function (amount) { + let data = rotatePoints( + feature.traps.paint().selectedItems[0].fillColor + ._components[1], + feature.traps.paint().selectedItems[0].fillColor + ._components[2], + amount + ); + + feature.traps.paint().selectedItems[0].fillColor._components[1].x = + data.finalP1.x; + feature.traps.paint().selectedItems[0].fillColor._components[1].y = + data.finalP1.y; + + feature.traps.paint().selectedItems[0].fillColor._components[2].x = + data.finalP2.x; + feature.traps.paint().selectedItems[0].fillColor._components[2].y = + data.finalP2.y; + }; + + function rotatePoints(p1, p2, angle) { + // Calculate the midpoint + const midpoint = { + x: (p1.x + p2.x) / 2, + y: (p1.y + p2.y) / 2, + }; + + const translatedP1 = { + x: p1.x - midpoint.x, + y: p1.y - midpoint.y, + }; + const translatedP2 = { + x: p2.x - midpoint.x, + y: p2.y - midpoint.y, + }; + + const radians = angle * (Math.PI / 180); + + const rotatedP1 = { + x: + translatedP1.x * Math.cos(radians) - translatedP1.y * Math.sin(radians), + y: + translatedP1.x * Math.sin(radians) + translatedP1.y * Math.cos(radians), + }; + const rotatedP2 = { + x: + translatedP2.x * Math.cos(radians) - translatedP2.y * Math.sin(radians), + y: + translatedP2.x * Math.sin(radians) + translatedP2.y * Math.cos(radians), + }; + + const finalP1 = { + x: rotatedP1.x + midpoint.x, + y: rotatedP1.y + midpoint.y, + }; + const finalP2 = { + x: rotatedP2.x + midpoint.x, + y: rotatedP2.y + midpoint.y, + }; + + return { finalP1, finalP2 }; + } + + function handleSlider(handle, value) { + let isDragging = false; + + handle.addEventListener("mousedown", (e) => { + isDragging = true; + const initialX = e.clientX; + const handleLeft = parseInt(handle.style.left) || 0; + + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + + function onMouseMove(e) { + if (isDragging) { + const offsetX = e.clientX - initialX; + let newLeft = handleLeft + offsetX; + + newLeft = Math.max(0, Math.min(124, newLeft)); + + rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation) + update() + lastRotation = Math.floor((newLeft / 124) * 360) + + value.textContent = + Math.floor((newLeft / 124) * 360).toString() + "deg"; + + handle.style.left = newLeft + "px"; + } + } + + function onMouseUp() { + isDragging = false; + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + } + }); + + handle.addEventListener("touchstart", (e) => { + isDragging = true; + const initialX = e.touches[0].clientX; + const handleLeft = parseInt(handle.style.left) || 0; + + handle.addEventListener("touchmove", onTouchMove); + handle.addEventListener("touchend", onTouchEnd); + + function onTouchMove(e) { + if (isDragging) { + const offsetX = e.touches[0].clientX - initialX; + let newLeft = handleLeft + offsetX; + + newLeft = Math.max(0, Math.min(124, newLeft)); + + rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation) + update() + lastRotation = Math.floor((newLeft / 124) * 360) + + value.textContent = + Math.floor((newLeft / 124) * 360).toString() + "deg"; + + handle.style.left = newLeft + "px"; + } + } + + function onTouchEnd() { + isDragging = false; + handle.removeEventListener("touchmove", onTouchMove); + handle.removeEventListener("touchend", onTouchEnd); + } + }); + } + + function update() { + feature.traps.getPaper().tool.onUpdateImage() + } +} diff --git a/features/rotate-gradient/style.css b/features/rotate-gradient/style.css new file mode 100644 index 00000000..03521129 --- /dev/null +++ b/features/rotate-gradient/style.css @@ -0,0 +1,27 @@ +.ste-direction-slider { + padding-top: 0.2rem; + margin-bottom: 20px; +} + +.ste-direction-slider-checkered { + background-color: #f6f6f6; + background-image: linear-gradient( + to right, + #c5ccd6, + #c5ccd6 1px, + transparent 1px, + transparent 1.5px + ); + background-size: 3px 100%; + background-position: 0 0, 10px 10px; +} + +.ste-direction-background { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 11px; +}