Skip to content

Commit f7a12ee

Browse files
authored
Merge pull request #895 from rgantzos/rotate-gradient
Rotate gradients
2 parents 256a3ad + e8a8fa5 commit f7a12ee

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed

features/features.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"version": 2,
4+
"id": "rotate-gradient",
5+
"versionAdded": "v4.0.0"
6+
},
27
{
38
"version": 2,
49
"id": "change-monitor-opacity",

features/rotate-gradient/data.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"title": "Rotate Gradients",
3+
"description": "Allows you to rotate gradients in any direction in the costume editor. Works in both the vector and bitmap editors.",
4+
"credits": [
5+
{
6+
"url": "https://scratch.mit.edu/users/rgantzos/",
7+
"username": "rgantzos"
8+
}
9+
],
10+
"type": ["Editor"],
11+
"dynamic": true,
12+
"scripts": [{ "file": "script.js", "runOn": "/projects/*" }],
13+
"styles": [{ "file": "style.css", "runOn": "/projects/*" }]
14+
}

features/rotate-gradient/script.js

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
export default async function ({ feature, console }) {
2+
let lastRotation = 0
3+
4+
feature.page.waitForElements(
5+
"div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']",
6+
function (row) {
7+
let body = document.querySelector(".Popover-body");
8+
9+
if (feature.traps.paint().selectedItems.length !== 1) return;
10+
if (feature.traps.paint().selectedItems[0].fillColor._components[0].radial) return;
11+
if (!document.querySelector("div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']")) return;
12+
if (body.querySelector(".ste-direction-slider")) return;
13+
14+
let div = document.createElement("div");
15+
div.className = "ste-direction-slider";
16+
feature.self.hideOnDisable(div);
17+
18+
let data = document.createElement("div");
19+
data.className = "color-picker_row-header_173LQ";
20+
div.appendChild(data);
21+
22+
let name = document.createElement("span");
23+
name.className = "color-picker_label-name_17igY";
24+
name.textContent = "Direction";
25+
26+
let value = document.createElement("span");
27+
value.className = "color-picker_label-readout_9vjb2";
28+
value.textContent = "0deg";
29+
30+
data.appendChild(name);
31+
data.appendChild(value);
32+
33+
let slider = document.createElement("div");
34+
slider.className =
35+
"ste-direction-slider-checkered slider_container_o2aIb slider_last_10jvO";
36+
div.appendChild(slider);
37+
38+
let sliderBg = document.createElement("div");
39+
sliderBg.style.background = `linear-gradient(270deg, ${
40+
feature.traps.paint().selectedItems[0]?.fillColor?._canvasStyle
41+
} 0%, rgba(0, 0, 0, 0) 100%)`;
42+
sliderBg.className = "ste-direction-background";
43+
slider.appendChild(sliderBg);
44+
45+
let handle = document.createElement("div");
46+
handleSlider(handle, value);
47+
handle.className = "ste-direction-handle slider_handle_3f0xk";
48+
handle.style.left = "124px";
49+
if (feature.traps.paint().selectedItems[0]?.opacity) {
50+
handle.style.left = "0px";
51+
}
52+
slider.appendChild(handle);
53+
54+
body.firstChild.insertBefore(div, body.querySelector("div[class^='color-picker_row-header_']").parentElement);
55+
lastRotation = 0
56+
}
57+
);
58+
59+
feature.redux.subscribe(function() {
60+
if (!document.querySelector("div[class^='paint-editor_editor-container_']")) return;
61+
62+
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) {
63+
document.querySelector(".ste-direction-slider")?.remove()
64+
}
65+
})
66+
67+
const rotateColor = function (amount) {
68+
let data = rotatePoints(
69+
feature.traps.paint().selectedItems[0].fillColor
70+
._components[1],
71+
feature.traps.paint().selectedItems[0].fillColor
72+
._components[2],
73+
amount
74+
);
75+
76+
feature.traps.paint().selectedItems[0].fillColor._components[1].x =
77+
data.finalP1.x;
78+
feature.traps.paint().selectedItems[0].fillColor._components[1].y =
79+
data.finalP1.y;
80+
81+
feature.traps.paint().selectedItems[0].fillColor._components[2].x =
82+
data.finalP2.x;
83+
feature.traps.paint().selectedItems[0].fillColor._components[2].y =
84+
data.finalP2.y;
85+
};
86+
87+
function rotatePoints(p1, p2, angle) {
88+
// Calculate the midpoint
89+
const midpoint = {
90+
x: (p1.x + p2.x) / 2,
91+
y: (p1.y + p2.y) / 2,
92+
};
93+
94+
const translatedP1 = {
95+
x: p1.x - midpoint.x,
96+
y: p1.y - midpoint.y,
97+
};
98+
const translatedP2 = {
99+
x: p2.x - midpoint.x,
100+
y: p2.y - midpoint.y,
101+
};
102+
103+
const radians = angle * (Math.PI / 180);
104+
105+
const rotatedP1 = {
106+
x:
107+
translatedP1.x * Math.cos(radians) - translatedP1.y * Math.sin(radians),
108+
y:
109+
translatedP1.x * Math.sin(radians) + translatedP1.y * Math.cos(radians),
110+
};
111+
const rotatedP2 = {
112+
x:
113+
translatedP2.x * Math.cos(radians) - translatedP2.y * Math.sin(radians),
114+
y:
115+
translatedP2.x * Math.sin(radians) + translatedP2.y * Math.cos(radians),
116+
};
117+
118+
const finalP1 = {
119+
x: rotatedP1.x + midpoint.x,
120+
y: rotatedP1.y + midpoint.y,
121+
};
122+
const finalP2 = {
123+
x: rotatedP2.x + midpoint.x,
124+
y: rotatedP2.y + midpoint.y,
125+
};
126+
127+
return { finalP1, finalP2 };
128+
}
129+
130+
function handleSlider(handle, value) {
131+
let isDragging = false;
132+
133+
handle.addEventListener("mousedown", (e) => {
134+
isDragging = true;
135+
const initialX = e.clientX;
136+
const handleLeft = parseInt(handle.style.left) || 0;
137+
138+
document.addEventListener("mousemove", onMouseMove);
139+
document.addEventListener("mouseup", onMouseUp);
140+
141+
function onMouseMove(e) {
142+
if (isDragging) {
143+
const offsetX = e.clientX - initialX;
144+
let newLeft = handleLeft + offsetX;
145+
146+
newLeft = Math.max(0, Math.min(124, newLeft));
147+
148+
rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation)
149+
update()
150+
lastRotation = Math.floor((newLeft / 124) * 360)
151+
152+
value.textContent =
153+
Math.floor((newLeft / 124) * 360).toString() + "deg";
154+
155+
handle.style.left = newLeft + "px";
156+
}
157+
}
158+
159+
function onMouseUp() {
160+
isDragging = false;
161+
document.removeEventListener("mousemove", onMouseMove);
162+
document.removeEventListener("mouseup", onMouseUp);
163+
}
164+
});
165+
166+
handle.addEventListener("touchstart", (e) => {
167+
isDragging = true;
168+
const initialX = e.touches[0].clientX;
169+
const handleLeft = parseInt(handle.style.left) || 0;
170+
171+
handle.addEventListener("touchmove", onTouchMove);
172+
handle.addEventListener("touchend", onTouchEnd);
173+
174+
function onTouchMove(e) {
175+
if (isDragging) {
176+
const offsetX = e.touches[0].clientX - initialX;
177+
let newLeft = handleLeft + offsetX;
178+
179+
newLeft = Math.max(0, Math.min(124, newLeft));
180+
181+
rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation)
182+
update()
183+
lastRotation = Math.floor((newLeft / 124) * 360)
184+
185+
value.textContent =
186+
Math.floor((newLeft / 124) * 360).toString() + "deg";
187+
188+
handle.style.left = newLeft + "px";
189+
}
190+
}
191+
192+
function onTouchEnd() {
193+
isDragging = false;
194+
handle.removeEventListener("touchmove", onTouchMove);
195+
handle.removeEventListener("touchend", onTouchEnd);
196+
}
197+
});
198+
}
199+
200+
function update() {
201+
feature.traps.getPaper().tool.onUpdateImage()
202+
}
203+
}

features/rotate-gradient/style.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.ste-direction-slider {
2+
padding-top: 0.2rem;
3+
margin-bottom: 20px;
4+
}
5+
6+
.ste-direction-slider-checkered {
7+
background-color: #f6f6f6;
8+
background-image: linear-gradient(
9+
to right,
10+
#c5ccd6,
11+
#c5ccd6 1px,
12+
transparent 1px,
13+
transparent 1.5px
14+
);
15+
background-size: 3px 100%;
16+
background-position: 0 0, 10px 10px;
17+
}
18+
19+
.ste-direction-background {
20+
content: "";
21+
position: absolute;
22+
top: 0;
23+
left: 0;
24+
width: 100%;
25+
height: 100%;
26+
border-radius: 11px;
27+
}

0 commit comments

Comments
 (0)