Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions features/features.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[
{
"version": 2,
"id": "rotate-gradient",
"versionAdded": "v4.0.0"
},
{
"version": 2,
"id": "change-monitor-opacity",
Expand Down
14 changes: 14 additions & 0 deletions features/rotate-gradient/data.json
Original file line number Diff line number Diff line change
@@ -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/*" }]
}
203 changes: 203 additions & 0 deletions features/rotate-gradient/script.js
Original file line number Diff line number Diff line change
@@ -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()
}
}
27 changes: 27 additions & 0 deletions features/rotate-gradient/style.css
Original file line number Diff line number Diff line change
@@ -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;
}