soiz1 commited on
Commit
3aea618
·
verified ·
1 Parent(s): 78805d2

Create userscript.js

Browse files
src/addons/addons/paint-gradient-maker/userscript.js ADDED
@@ -0,0 +1,1289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Gradient Maker Addon
2
+ // By: SharkPool
3
+ export default async function () {
4
+ const isPM = true;
5
+ const customID = "custom-gradient-btn";
6
+ const symbolTag = Symbol("custom-gradient-tag");
7
+ const guiIMGS = {
8
+ "select": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect stroke="#000" fill="#fff" x=".5" y=".5" width="19" height="19" rx="4" stroke-opacity=".15"/><path fill="red" d="M13.35 8.8h-2.4V6.4a1.2 1.2 90 0 0-2.4 0l.043 2.4H6.15a1.2 1.2 90 0 0 0 2.4l2.443-.043L8.55 13.6a1.2 1.2 90 0 0 2.4 0v-2.443l2.4.043a1.2 1.2 90 0 0 0-2.4"/></svg>`,
9
+ "add": `<svg viewBox="2 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M18 10h-4V6a2 2 0 0 0-4 0l.071 4H6a2 2 0 0 0 0 4l4.071-.071L10 18a2 2 0 0 0 4 0v-4.071L18 14a2 2 0 0 0 0-4"></path></svg>`,
10
+ "delete": `<svg viewBox="2 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M 18 10 h -4 H 6 a 2 2 0 0 0 0 4 L 18 14 a 2 2 0 0 0 0 -4"></path></svg>`,
11
+ };
12
+
13
+ const paperLinkModes = new Set([
14
+ "TEXT", "OVAL", "RECT",
15
+ ...(isPM ? ["ROUNDED_RECT", "TRIANGLE", "SUSSY", "ARROW"] : [])
16
+ ]);
17
+
18
+ let selectedClassName, unselectedClassName, customBtn;
19
+ let observerUsed = false;
20
+ let modalStorage = {};
21
+
22
+ /* Internal Utils */
23
+ function position2Angle(p1, p2) {
24
+ const dx = p1.x - p2.x;
25
+ const dy = p1.y - p2.y;
26
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI);
27
+ return angle + 90;
28
+ }
29
+
30
+ function initGradSelectClasses(gradRow) {
31
+ const classes = {};
32
+ const children = Array.from(gradRow.children);
33
+ for (const child of children) {
34
+ const name = child.classList.toString();
35
+ if (classes[name] === undefined) classes[name] = 1;
36
+ else classes[name] = 0;
37
+ }
38
+
39
+ for (const [cls, count] of Object.entries(classes)) {
40
+ if (count) selectedClassName = cls;
41
+ else unselectedClassName = cls
42
+ }
43
+ }
44
+
45
+ function encodeGradHTML(settings) {
46
+ const sortedParts = [...settings.parts].sort((a, b) => a.p - b.p);
47
+
48
+ let gradString = settings.type === "Linear" ? "linear-gradient(" : "radial-gradient(";
49
+ if (settings.type === "Linear") gradString += `${settings.dir}deg, `;
50
+ for (const part of sortedParts) gradString += `${part.c} ${part.p}%, `;
51
+ return gradString.substring(0, gradString.length - 2) + ")";
52
+ }
53
+
54
+ function genLinearGradPoints(bounds, angleDeg) {
55
+ const center = bounds.center;
56
+ const dir = new paper.Point({ angle: angleDeg, length: 1 });
57
+ const boundsRect = new paper.Path.Rectangle(bounds);
58
+ const gradLine = new paper.Path.Line({
59
+ from: center.subtract(dir.multiply(10000)),
60
+ to: center.add(dir.multiply(10000))
61
+ });
62
+
63
+ const intersections = gradLine.getIntersections(boundsRect);
64
+ gradLine.remove();
65
+ boundsRect.remove();
66
+ if (intersections.length < 2) {
67
+ return {
68
+ origin: center.subtract(dir.multiply(bounds.width / 2)),
69
+ destination: center.add(dir.multiply(bounds.width / 2))
70
+ };
71
+ } else {
72
+ return {
73
+ origin: intersections[0].point,
74
+ destination: intersections[1].point
75
+ };
76
+ }
77
+ }
78
+
79
+ function setSelected2Grad(settings) {
80
+ // compile SVG-based gradient
81
+ const sortedParts = [...settings.parts].sort((a, b) => a.p - b.p);
82
+ const gradStops = sortedParts.map(part => new paper.GradientStop(part.c, part.p / 100));
83
+ const gradient = new paper.Gradient(gradStops, settings.type === "Radial");
84
+ modalStorage._gradCache = { settings, gradient };
85
+
86
+ paper.project.getSelectedItems().forEach((item) => {
87
+ let origin, destination;
88
+ if (settings.type === "Radial") {
89
+ origin = item.bounds.center;
90
+ destination = item.bounds.center.add([item.bounds.width / 2, 0]);
91
+ } else {
92
+ const points = genLinearGradPoints(item.bounds, settings.dir - 90);
93
+ origin = points.origin;
94
+ destination = points.destination;
95
+ }
96
+
97
+ item[settings.path] = { gradient, origin, destination };
98
+ });
99
+
100
+ // update drawing & action
101
+ if (paper.tool.onUpdateImage) paper.tool.onUpdateImage();
102
+
103
+ // set with html otherwise GUI will crash
104
+ const swatch = document.querySelectorAll(
105
+ `div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`
106
+ )[settings.path === "fillColor" ? 0 : 1];
107
+ if (swatch) swatch.style.background = encodeGradHTML(settings);
108
+ }
109
+
110
+ function paperGrad2CSS(paperGrad) {
111
+ const { gradient, origin, destination } = paperGrad;
112
+ if (!gradient || !origin || !destination) return null;
113
+
114
+ const stops = gradient.stops.map(s => `${s.color.toCSS(true)} ${Math.round(s.offset * 100)}%`);
115
+ if (gradient.radial) return `radial-gradient(circle, ${stops.join(", ")})`;
116
+ else return `linear-gradient(${position2Angle(destination, origin)}deg, ${stops.join(", ")})`;
117
+ }
118
+
119
+ function extractGradient(color) {
120
+ if (!color || !color.gradient) return {};
121
+ return {
122
+ gradient: color.gradient,
123
+ origin: color.origin || "",
124
+ destination: color.destination || color.highlight || ""
125
+ };
126
+ }
127
+
128
+ function decodeSelectedGrad(item, draggableDiv, settingsDiv) {
129
+ const { gradient, origin, destination } = extractGradient(item[modalStorage.path]);
130
+ if (!gradient || !origin || !destination) return draggableDiv.append(createDraggable(), createDraggable());
131
+
132
+ // create draggables
133
+ const newStops = gradient.stops.map((s, i) => {
134
+ // "offset" will be undefined when using Scratch gradients, which dont have set-stops
135
+ const alpha = Math.round(s.color.alpha * 255).toString(16).padStart(2, "0");
136
+ return createDraggable(s.color.toCSS(true) + alpha, s.offset ? s.offset * 100 : i * 100)
137
+ });
138
+ draggableDiv.append(...newStops);
139
+
140
+ // preset values
141
+ const angle = position2Angle(destination, origin);
142
+ settingsDiv.querySelector("select").value = gradient.radial ? "Radial" : "Linear";
143
+ settingsDiv.querySelector("input").value = angle;
144
+ modalStorage.type = gradient.radial ? "Radial" : "Linear";
145
+ modalStorage.dir = angle;
146
+ }
147
+
148
+ function decodeFromCache(settings, draggableDiv, settingsDiv) {
149
+ // create draggables
150
+ const newStops = settings.parts.map((s, i) => {
151
+ // "p" will be NaN when using Scratch gradients, which dont have set-stops
152
+ return createDraggable(s.c, isNaN(s.p) ? i * 100 : s.p)
153
+ });
154
+ draggableDiv.append(...newStops);
155
+
156
+ // preset values
157
+ settingsDiv.querySelector("select").value = settings.type;
158
+ settingsDiv.querySelector("input").value = settings.dir;
159
+ modalStorage.type = settings.type;
160
+ modalStorage.dir = settings.dir;
161
+ }
162
+
163
+ function handleFillEvent() {
164
+ if (!modalStorage._gradCache) return;
165
+
166
+ // set the swatch color in case the GUI resets it
167
+ const swatch = document.querySelector(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
168
+ if (swatch) queueMicrotask(() => {
169
+ if (!modalStorage._gradCache) return;
170
+ swatch.style.background = encodeGradHTML(modalStorage._gradCache.settings);
171
+ });
172
+
173
+ const tool = paper.tool;
174
+ if (typeof tool?._getFillItem !== "function") return;
175
+
176
+ const item = tool._getFillItem();
177
+ if (!item) return;
178
+
179
+ const bounds = item.bounds;
180
+ let origin, destination;
181
+ if (modalStorage.type === "Radial") {
182
+ origin = new paper.Point(tool._point.x, tool._point.y);
183
+ destination = origin.add([Math.max(bounds.width, bounds.height) / 2, 0]);
184
+ } else {
185
+ const points = genLinearGradPoints(bounds, modalStorage.dir - 90);
186
+ origin = points.origin;
187
+ destination = points.destination;
188
+ }
189
+
190
+ const path = tool.fillProperty === "fill" ? "fillColor" : "strokeColor";
191
+ item[path] = {
192
+ gradient: modalStorage._gradCache.gradient,
193
+ origin, destination
194
+ };
195
+ }
196
+
197
+ function handleShapeModeEvent(type) {
198
+ if (!modalStorage._gradCache && type !== "TEXT") return;
199
+
200
+ // set the swatch color in case the GUI resets it
201
+ const swatch = document.querySelector(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
202
+ if (swatch) queueMicrotask(() => {
203
+ if (!modalStorage._gradCache) return;
204
+ swatch.style.background = encodeGradHTML(modalStorage._gradCache.settings);
205
+ });
206
+
207
+ const tool = paper.tool;
208
+ if (typeof tool?._onMouseDrag !== "function") return;
209
+ if (tool[symbolTag]) return;
210
+ // patch this event, if not already, to run our code
211
+
212
+ const funcName = type === "TEXT" ? "onKeyDown" : "onMouseDrag";
213
+ const ogOnFunc = tool[funcName];
214
+ tool[symbolTag] = true;
215
+ tool[funcName] = function (...args) {
216
+ ogOnFunc.call(this, ...args);
217
+
218
+ // replace the fill with the custom gradient
219
+ if (!modalStorage._gradCache) {
220
+ if (type === "TEXT") {
221
+ tool.element.style.background = "";
222
+ tool.element.style.backgroundClip = "";
223
+ tool.element.style.color = "";
224
+ }
225
+ return;
226
+ }
227
+
228
+ let item;
229
+ switch (type) {
230
+ case "RECT":
231
+ item = this.rect;
232
+ break;
233
+ case "OVAL":
234
+ item = this.oval;
235
+ break;
236
+ case "TEXT":
237
+ item = this.textBox;
238
+ break;
239
+ /* PenguinMod shapes */
240
+ case "ROUNDED_RECT":
241
+ item = this.rect;
242
+ break;
243
+ case "TRIANGLE":
244
+ item = this.tri;
245
+ break;
246
+ case "SUSSY":
247
+ item = this.sussy;
248
+ break;
249
+ case "ARROW":
250
+ item = this.tri;
251
+ break;
252
+ default: return;
253
+ }
254
+ if (!item) return;
255
+ const bounds = item.bounds;
256
+ let origin, destination;
257
+ if (modalStorage.type === "Radial") {
258
+ origin = item.bounds.center;
259
+ destination = item.bounds.center.add([item.bounds.width / 2, 0]);
260
+ } else {
261
+ const points = genLinearGradPoints(bounds, modalStorage.dir - 90);
262
+ origin = points.origin;
263
+ destination = points.destination;
264
+ }
265
+
266
+ item.fillColor = {
267
+ gradient: modalStorage._gradCache.gradient,
268
+ origin, destination
269
+ };
270
+
271
+ // text uses HTML elements, so we have to handle that too
272
+ if (type === "TEXT") {
273
+ tool.element.style.background = encodeGradHTML(modalStorage._gradCache.settings);
274
+ tool.element.style.backgroundClip = "text";
275
+ tool.element.style.color = "transparent";
276
+ }
277
+ }
278
+ }
279
+
280
+ /* GUI Utils */
281
+ function getButtonURI(name, dontCompile) {
282
+ const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
283
+ const guiSVG = guiIMGS[name].replace("red", themeHex);
284
+ if (dontCompile) return guiSVG;
285
+ else return "data:image/svg+xml;base64," + btoa(guiSVG);
286
+ }
287
+
288
+ function showSelectedGrad(item) {
289
+ const [fillSwatch, outlineSwatch] = document.querySelectorAll(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
290
+ const outCSSGrad = paperGrad2CSS(extractGradient(item.strokeColor));
291
+ if (outlineSwatch) {
292
+ if (outCSSGrad) outlineSwatch.style.background = outCSSGrad;
293
+ else if (!item.strokeColor || item.strokeWidth === 0) outlineSwatch.style.background = "#fff";
294
+ }
295
+
296
+ const fillGrad = extractGradient(item.fillColor);
297
+ const fillCSSGrad = paperGrad2CSS(fillGrad);
298
+ modalStorage._gradCache = undefined;
299
+ if (fillSwatch) {
300
+ if (fillCSSGrad) {
301
+ fillSwatch.style.background = fillCSSGrad;
302
+
303
+ // update cache
304
+ const { gradient, destination, origin } = fillGrad;
305
+ modalStorage._gradCache = {
306
+ gradient,
307
+ settings: {
308
+ type: gradient.radial ? "Radial" : "Linear",
309
+ dir: position2Angle(destination, origin),
310
+ parts: gradient.stops.map(s => {
311
+ const alpha = Math.round(s.color.alpha * 255).toString(16).padStart(2, "0");
312
+ return { c: s.color.toCSS(true) + alpha, p: s.offset * 100 };
313
+ })
314
+ }
315
+ };
316
+ } else if (!item.fillColor) fillSwatch.style.background = "#fff";
317
+ }
318
+ }
319
+
320
+ function createDraggable(optC, optP) {
321
+ const index = modalStorage.parts.length;
322
+ const rngPos = optP ?? Math.floor(Math.random() * 100);
323
+ const rngHex = optC ?? `#${Math.floor(Math.random() * Math.pow(2, 24)).toString(16).padStart(6, "0")}`;
324
+ const opacity = optC ? optC.length === 9 ? parseInt(optC.slice(7, 9), 16) / 255 : 1 : 1;
325
+
326
+ const draggable = document.createElement("div");
327
+ draggable.id = index;
328
+ draggable.classList.add("pointer");
329
+ draggable.setAttribute("style", `cursor: pointer; width: 25px; position: absolute; top: -6px; transform: translateX(-50%);`);
330
+ draggable.style.left = `${rngPos}%`;
331
+
332
+ const nub = document.createElementNS("http://www.w3.org/2000/svg", "svg");
333
+ nub.setAttribute("width", "14");
334
+ nub.setAttribute("height", "7");
335
+ nub.style.transform = "translateX(45%)";
336
+
337
+ const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
338
+ polygon.setAttribute("points", "0,7 7,0 14,7");
339
+ polygon.setAttribute("stroke", "#fff");
340
+ polygon.setAttribute("fill", "#fff");
341
+ nub.appendChild(polygon);
342
+
343
+ const color = document.createElement("div");
344
+ color.setAttribute("style", `width: 25px; height: 25px; border-radius: 4px; background: #fff; display: flex; justify-content: center; align-items: center; flex-direction: column;`);
345
+
346
+ const colorContainer = document.createElement("div");
347
+ colorContainer.setAttribute("style", `width: 16px; height: 16px; border-radius: 5px; background: ${rngHex}; border: solid 2px rgba(0,0,0,.2); opacity: ${opacity}; margin-bottom: 2px;`);
348
+
349
+ const colorInput = document.createElement("input");
350
+ colorInput.setAttribute("type", "color");
351
+ colorInput.setAttribute("style", `opacity: 0; position: absolute; pointer-events: none;`);
352
+
353
+ const opacityInput = document.createElement("input");
354
+ opacityInput.setAttribute("type", "number");
355
+ opacityInput.setAttribute("min", "0");
356
+ opacityInput.setAttribute("max", "100");
357
+ opacityInput.value = opacity * 100;
358
+ opacityInput.setAttribute("style", `visibility: hidden; background: #fff; border: none; color: #000; text-align: center; position: absolute; pointer-events: auto; width: 45px; height: 25px; padding: 0; margin: 0; border-radius: 0 5px 5px 0; left: 22px;`);
359
+
360
+ // Color picker handler
361
+ colorContainer.addEventListener("click", (e) => {
362
+ opacityInput.style.visibility = "visible";
363
+ colorInput.click();
364
+ e.stopPropagation();
365
+ });
366
+ draggable.addEventListener("mouseleave", (e) => {
367
+ opacityInput.style.visibility = "hidden";
368
+ e.stopPropagation();
369
+ });
370
+
371
+ colorInput.addEventListener("input", (e) => {
372
+ modalStorage.parts[index].c = e.target.value;
373
+ colorContainer.style.background = e.target.value;
374
+ updateDisplay();
375
+ });
376
+
377
+ // Opacity slider handler
378
+ opacityInput.addEventListener("click", (e) => {
379
+ opacityInput.focus();
380
+ e.stopPropagation();
381
+ });
382
+ opacityInput.addEventListener("input", (e) => {
383
+ const newOpacity = Math.min(100, Math.max(0, e.target.value));
384
+ e.target.value = newOpacity;
385
+ colorContainer.style.opacity = newOpacity / 100;
386
+
387
+ const alpha = Math.round(newOpacity * 2.55).toString(16).padStart(2, "0");
388
+ const hex = modalStorage.parts[index].c;
389
+ modalStorage.parts[index].c = hex.substring(0, 7) + alpha;
390
+ updateDisplay();
391
+ });
392
+
393
+ draggable.addEventListener("mousedown", (e) => {
394
+ e.preventDefault();
395
+ if (e.target === opacityInput) return;
396
+
397
+ modalStorage.selectedPointer = draggable;
398
+ const container = draggable.parentElement;
399
+ const containerRect = container.getBoundingClientRect();
400
+
401
+ const onMouseMove = (moveEvent) => {
402
+ const x = moveEvent.clientX - containerRect.left;
403
+ const percent = Math.min(100, Math.max(0, (x / container.offsetWidth) * 100));
404
+ draggable.style.left = `${percent}%`;
405
+ modalStorage.parts[index].p = percent;
406
+ updateDisplay();
407
+ };
408
+
409
+ const onMouseUp = () => {
410
+ document.removeEventListener("mousemove", onMouseMove);
411
+ document.removeEventListener("mouseup", onMouseUp);
412
+ };
413
+
414
+ document.addEventListener("mousemove", onMouseMove);
415
+ document.addEventListener("mouseup", onMouseUp);
416
+ });
417
+
418
+ color.append(colorContainer, colorInput, opacityInput);
419
+ draggable.append(nub, color);
420
+ modalStorage.parts.push({ c: rngHex, p: rngPos });
421
+ modalStorage.selectedPointer = draggable;
422
+ return draggable;
423
+ }
424
+
425
+ function genSettingsTable(div) {
426
+ const btnStyle = `width: 35px; height: 35px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--paint-input-background, --ui-primary, #fff); transition: transform 0.2s;`;
427
+ const selectStlye = `cursor: pointer; height: 30px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--ui-secondary, #fff);`;
428
+ const directionStyle = `text-align: center; width: 50px; height: 25px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--ui-secondary, #fff);`;
429
+
430
+ const createBtn = document.createElement("button");
431
+ createBtn.setAttribute("style", btnStyle);
432
+ createBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
433
+ createBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
434
+ createBtn.innerHTML = getButtonURI("add", true);
435
+ createBtn.addEventListener("click", (e) => {
436
+ const draggableSpace = modalStorage.modal.querySelector(`div[class="draggables"]`);
437
+ draggableSpace.appendChild(createDraggable());
438
+ updateDisplay();
439
+ e.stopPropagation();
440
+ });
441
+
442
+ const deleteBtn = document.createElement("button");
443
+ deleteBtn.setAttribute("style", btnStyle);
444
+ deleteBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
445
+ deleteBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
446
+ deleteBtn.style.margin = "0px 8px";
447
+ deleteBtn.innerHTML = getButtonURI("delete", true);
448
+ deleteBtn.addEventListener("click", (e) => {
449
+ const pointer = modalStorage.selectedPointer;
450
+ if (pointer) {
451
+ modalStorage.parts.splice(pointer.id, 1);
452
+ pointer.remove();
453
+ updateDisplay();
454
+ delete modalStorage.selectedPointer;
455
+ }
456
+ e.stopPropagation();
457
+ });
458
+
459
+ const title1 = document.createElement("span");
460
+ title1.textContent = "Gradient Type:";
461
+
462
+ const select = document.createElement("select");
463
+ select.setAttribute("style", selectStlye);
464
+
465
+ const option1 = document.createElement("option");
466
+ const option2 = document.createElement("option");
467
+ option1.text = "Linear"; option1.value = "Linear";
468
+ option2.text = "Radial"; option2.value = "Radial";
469
+ select.append(option1, option2);
470
+ select.addEventListener("change", (e) => {
471
+ modalStorage.type = e.target.value;
472
+ updateDisplay();
473
+ e.stopPropagation();
474
+ });
475
+
476
+ const title2 = document.createElement("span");
477
+ title2.textContent = "Direction:";
478
+
479
+ const dirBtn = document.createElement("input");
480
+ dirBtn.setAttribute("style", directionStyle);
481
+ dirBtn.setAttribute("type", "number");
482
+ dirBtn.setAttribute("max", 360);
483
+ dirBtn.setAttribute("min", 0);
484
+ dirBtn.setAttribute("value", 90);
485
+ dirBtn.addEventListener("input", (e) => {
486
+ modalStorage.dir = e.target.value;
487
+ updateDisplay();
488
+ e.stopPropagation();
489
+ });
490
+
491
+ div.append(createBtn, deleteBtn, title1, select, title2, dirBtn);
492
+ }
493
+
494
+ function genButtonTable(div) {
495
+ const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
496
+ const btnStyle = `color: #fff; font-weight: 600; text-align: center; padding: 10px; margin: 10px 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: ${themeHex}; transition: transform 0.2s;`;
497
+
498
+ const enterBtn = document.createElement("button");
499
+ enterBtn.id = "enter";
500
+ enterBtn.setAttribute("style", btnStyle);
501
+ enterBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
502
+ enterBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
503
+ enterBtn.textContent = "Okay";
504
+
505
+ const cancelBtn = document.createElement("button");
506
+ cancelBtn.id = "cancel";
507
+ cancelBtn.setAttribute("style", btnStyle);
508
+ cancelBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
509
+ cancelBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
510
+ cancelBtn.textContent = "Cancel";
511
+
512
+ div.append(cancelBtn, enterBtn);
513
+ }
514
+
515
+ function updateDisplay() {
516
+ const display = modalStorage.modal.querySelector(`div[class="color-display"]`);
517
+ display.style.background = encodeGradHTML(modalStorage);
518
+ }
519
+
520
+ /* Main GUI */
521
+ function openGradientMaker() {
522
+ const paint = ReduxStore.getState().scratchPaint;
523
+ const oldCache = modalStorage._gradCache;
524
+ modalStorage = {
525
+ parts: [], type: "Linear", dir: 90,
526
+ selectedPointer: undefined, modal: undefined,
527
+ path: paint.modals.fillColor ? "fillColor" : "strokeColor"
528
+ };
529
+
530
+ const container = document.createElement("div");
531
+ container.classList.add("SP-gradient-maker");
532
+ container.setAttribute("style", `position: absolute; z-index: 9999; pointer-events: auto; background-color: rgba(0,0,0,.1); width: 100%; height: 100vh;`);
533
+
534
+ const modal = document.createElement("div");
535
+ modal.classList.add("gradient-modal");
536
+ modal.setAttribute("style", `color: var(--paint-text-primary, #575e75); width: 450px; height: 260px; display: block; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); background: var(--ui-secondary, hsla(215, 75%, 95%, 1)); border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; padding: 15px;`);
537
+ modalStorage.modal = modal;
538
+
539
+ const title = document.createElement("span");
540
+ title.setAttribute("style", `display: block; text-align: center; justify-content: center; border-bottom: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); padding-bottom: 10px; margin: 0 25px 0 25px; font-weight: 600;"`);
541
+ title.textContent = "Gradient Maker";
542
+
543
+ const display = document.createElement("div");
544
+ display.classList.add("color-display");
545
+ display.setAttribute("style", `width: 420px; height: 40px; display: flex; justify-content: center; align-items: center; margin: 15px 15px 0 15px; border: solid 2px grey; border-radius: 5px 5px 0 0;`);
546
+
547
+ const draggables = document.createElement("div");
548
+ draggables.classList.add("draggables");
549
+ draggables.setAttribute("style", `width: 420px; height: 40px; position: relative; display: flex; justify-content: center; align-items: center; margin: 0 15px 15px 15px; border: solid 2px grey; border-radius: 0 0 5px 5px; background: #111111;`);
550
+
551
+ const settings = document.createElement("div");
552
+ settings.classList.add("settings");
553
+ settings.setAttribute("style", `border-top: dashed 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); padding-top: 10px; display: flex; justify-content: center; align-items: center;`);
554
+ genSettingsTable(settings);
555
+
556
+ const buttons = document.createElement("div");
557
+ buttons.setAttribute("style", `display: flex; justify-content: center; align-items: center;`);
558
+ genButtonTable(buttons);
559
+ buttons.addEventListener("click", (e) => {
560
+ if (e.target.id) {
561
+ if (e.target.id === "enter") setSelected2Grad(modalStorage);
562
+ container.remove();
563
+ }
564
+ e.stopPropagation();
565
+ });
566
+
567
+ modal.append(title, display, draggables, settings, buttons);
568
+ container.appendChild(modal);
569
+ document.body.appendChild(container);
570
+
571
+ if (paint.selectedItems?.length) decodeSelectedGrad(paint.selectedItems[0], draggables, settings);
572
+ else if (oldCache) decodeFromCache(oldCache.settings, draggables, settings);
573
+ else draggables.append(createDraggable(), createDraggable());
574
+ updateDisplay();
575
+ }
576
+
577
+ function startListenerWorker() {
578
+ let lastMode, lastSelected, lastModals;
579
+ ReduxStore.subscribe(() => {
580
+ const paint = ReduxStore.getState().scratchPaint;
581
+ if (!paint || paint?.format === undefined || paint?.format === null) return;
582
+ const { mode, selectedItems, modals } = paint;
583
+
584
+ // no bitmap support :(
585
+ if (paint.format.startsWith("BITMAP")) {
586
+ if (customBtn) {
587
+ customBtn.remove();
588
+ customBtn = undefined;
589
+ }
590
+ return;
591
+ }
592
+
593
+ // run relative tool events
594
+ if (mode === "FILL") handleFillEvent();
595
+ else if (paperLinkModes.has(mode)) handleShapeModeEvent(mode);
596
+
597
+ const idChain = selectedItems.map((e) => e.id).join(".");
598
+ const modalChain = `${modals.fillColor}${modals.strokeColor}`;
599
+ if (mode === lastMode && idChain === lastSelected && modalChain === lastModals) return;
600
+ lastMode = mode;
601
+ lastSelected = idChain;
602
+ lastModals = modalChain;
603
+
604
+ // decode potential custom gradients
605
+ if (selectedItems?.length) showSelectedGrad(selectedItems[0]);
606
+ else if (mode === "SELECT" || mode === "RESHAPE") modalStorage._gradCache = undefined;
607
+
608
+ // add custom modal
609
+ if (!modals.strokeColor && !modals.fillColor) return;
610
+ if (observerUsed) return;
611
+ const observer = new MutationObserver(() => {
612
+ const gradRow = document.querySelector(`div[class^="color-picker_gradient-picker-row_"]`);
613
+ if (!gradRow || gradRow.lastElementChild.id === customID) return;
614
+
615
+ // get the appropriate class names for selected items
616
+ if (!selectedClassName) initGradSelectClasses(gradRow);
617
+ const children = Array.from(gradRow.children);
618
+
619
+ customBtn = children[0].cloneNode(true);
620
+ customBtn.src = getButtonURI("select");
621
+ customBtn.id = customID;
622
+ customBtn.setAttribute("class", unselectedClassName);
623
+ gradRow.appendChild(customBtn);
624
+
625
+ gradRow.addEventListener("click", (e) => {
626
+ if (e.target === customBtn) {
627
+ for (const child of children) child.setAttribute("class", unselectedClassName);
628
+ customBtn.setAttribute("class", selectedClassName);
629
+ openGradientMaker();
630
+ } else if (e.target.nodeName === "IMG") {
631
+ customBtn.setAttribute("class", unselectedClassName);
632
+ }
633
+ });
634
+
635
+ observerUsed = false;
636
+ observer.disconnect();
637
+ });
638
+
639
+ observer.observe(document.body, { childList: true, subtree: true });
640
+ observerUsed = true;
641
+ });
642
+ }
643
+
644
+ if (typeof scaffolding === "undefined") startListenerWorker();
645
+ }// Gradient Maker Addon
646
+ // By: SharkPool
647
+ export default async function () {
648
+ const isPM = true;
649
+ const customID = "custom-gradient-btn";
650
+ const symbolTag = Symbol("custom-gradient-tag");
651
+ const guiIMGS = {
652
+ "select": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect stroke="#000" fill="#fff" x=".5" y=".5" width="19" height="19" rx="4" stroke-opacity=".15"/><path fill="red" d="M13.35 8.8h-2.4V6.4a1.2 1.2 90 0 0-2.4 0l.043 2.4H6.15a1.2 1.2 90 0 0 0 2.4l2.443-.043L8.55 13.6a1.2 1.2 90 0 0 2.4 0v-2.443l2.4.043a1.2 1.2 90 0 0 0-2.4"/></svg>`,
653
+ "add": `<svg viewBox="2 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M18 10h-4V6a2 2 0 0 0-4 0l.071 4H6a2 2 0 0 0 0 4l4.071-.071L10 18a2 2 0 0 0 4 0v-4.071L18 14a2 2 0 0 0 0-4"></path></svg>`,
654
+ "delete": `<svg viewBox="2 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M 18 10 h -4 H 6 a 2 2 0 0 0 0 4 L 18 14 a 2 2 0 0 0 0 -4"></path></svg>`,
655
+ };
656
+
657
+ const paperLinkModes = new Set([
658
+ "TEXT", "OVAL", "RECT",
659
+ ...(isPM ? ["ROUNDED_RECT", "TRIANGLE", "SUSSY", "ARROW"] : [])
660
+ ]);
661
+
662
+ let selectedClassName, unselectedClassName, customBtn;
663
+ let observerUsed = false;
664
+ let modalStorage = {};
665
+
666
+ /* Internal Utils */
667
+ function position2Angle(p1, p2) {
668
+ const dx = p1.x - p2.x;
669
+ const dy = p1.y - p2.y;
670
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI);
671
+ return angle + 90;
672
+ }
673
+
674
+ function initGradSelectClasses(gradRow) {
675
+ const classes = {};
676
+ const children = Array.from(gradRow.children);
677
+ for (const child of children) {
678
+ const name = child.classList.toString();
679
+ if (classes[name] === undefined) classes[name] = 1;
680
+ else classes[name] = 0;
681
+ }
682
+
683
+ for (const [cls, count] of Object.entries(classes)) {
684
+ if (count) selectedClassName = cls;
685
+ else unselectedClassName = cls
686
+ }
687
+ }
688
+
689
+ function encodeGradHTML(settings) {
690
+ const sortedParts = [...settings.parts].sort((a, b) => a.p - b.p);
691
+
692
+ let gradString = settings.type === "Linear" ? "linear-gradient(" : "radial-gradient(";
693
+ if (settings.type === "Linear") gradString += `${settings.dir}deg, `;
694
+ for (const part of sortedParts) gradString += `${part.c} ${part.p}%, `;
695
+ return gradString.substring(0, gradString.length - 2) + ")";
696
+ }
697
+
698
+ function genLinearGradPoints(bounds, angleDeg) {
699
+ const center = bounds.center;
700
+ const dir = new paper.Point({ angle: angleDeg, length: 1 });
701
+ const boundsRect = new paper.Path.Rectangle(bounds);
702
+ const gradLine = new paper.Path.Line({
703
+ from: center.subtract(dir.multiply(10000)),
704
+ to: center.add(dir.multiply(10000))
705
+ });
706
+
707
+ const intersections = gradLine.getIntersections(boundsRect);
708
+ gradLine.remove();
709
+ boundsRect.remove();
710
+ if (intersections.length < 2) {
711
+ return {
712
+ origin: center.subtract(dir.multiply(bounds.width / 2)),
713
+ destination: center.add(dir.multiply(bounds.width / 2))
714
+ };
715
+ } else {
716
+ return {
717
+ origin: intersections[0].point,
718
+ destination: intersections[1].point
719
+ };
720
+ }
721
+ }
722
+
723
+ function setSelected2Grad(settings) {
724
+ // compile SVG-based gradient
725
+ const sortedParts = [...settings.parts].sort((a, b) => a.p - b.p);
726
+ const gradStops = sortedParts.map(part => new paper.GradientStop(part.c, part.p / 100));
727
+ const gradient = new paper.Gradient(gradStops, settings.type === "Radial");
728
+ modalStorage._gradCache = { settings, gradient };
729
+
730
+ paper.project.getSelectedItems().forEach((item) => {
731
+ let origin, destination;
732
+ if (settings.type === "Radial") {
733
+ origin = item.bounds.center;
734
+ destination = item.bounds.center.add([item.bounds.width / 2, 0]);
735
+ } else {
736
+ const points = genLinearGradPoints(item.bounds, settings.dir - 90);
737
+ origin = points.origin;
738
+ destination = points.destination;
739
+ }
740
+
741
+ item[settings.path] = { gradient, origin, destination };
742
+ });
743
+
744
+ // update drawing & action
745
+ if (paper.tool.onUpdateImage) paper.tool.onUpdateImage();
746
+
747
+ // set with html otherwise GUI will crash
748
+ const swatch = document.querySelectorAll(
749
+ `div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`
750
+ )[settings.path === "fillColor" ? 0 : 1];
751
+ if (swatch) swatch.style.background = encodeGradHTML(settings);
752
+ }
753
+
754
+ function paperGrad2CSS(paperGrad) {
755
+ const { gradient, origin, destination } = paperGrad;
756
+ if (!gradient || !origin || !destination) return null;
757
+
758
+ const stops = gradient.stops.map(s => `${s.color.toCSS(true)} ${Math.round(s.offset * 100)}%`);
759
+ if (gradient.radial) return `radial-gradient(circle, ${stops.join(", ")})`;
760
+ else return `linear-gradient(${position2Angle(destination, origin)}deg, ${stops.join(", ")})`;
761
+ }
762
+
763
+ function extractGradient(color) {
764
+ if (!color || !color.gradient) return {};
765
+ return {
766
+ gradient: color.gradient,
767
+ origin: color.origin || "",
768
+ destination: color.destination || color.highlight || ""
769
+ };
770
+ }
771
+
772
+ function decodeSelectedGrad(item, draggableDiv, settingsDiv) {
773
+ const { gradient, origin, destination } = extractGradient(item[modalStorage.path]);
774
+ if (!gradient || !origin || !destination) return draggableDiv.append(createDraggable(), createDraggable());
775
+
776
+ // create draggables
777
+ const newStops = gradient.stops.map((s, i) => {
778
+ // "offset" will be undefined when using Scratch gradients, which dont have set-stops
779
+ const alpha = Math.round(s.color.alpha * 255).toString(16).padStart(2, "0");
780
+ return createDraggable(s.color.toCSS(true) + alpha, s.offset ? s.offset * 100 : i * 100)
781
+ });
782
+ draggableDiv.append(...newStops);
783
+
784
+ // preset values
785
+ const angle = position2Angle(destination, origin);
786
+ settingsDiv.querySelector("select").value = gradient.radial ? "Radial" : "Linear";
787
+ settingsDiv.querySelector("input").value = angle;
788
+ modalStorage.type = gradient.radial ? "Radial" : "Linear";
789
+ modalStorage.dir = angle;
790
+ }
791
+
792
+ function decodeFromCache(settings, draggableDiv, settingsDiv) {
793
+ // create draggables
794
+ const newStops = settings.parts.map((s, i) => {
795
+ // "p" will be NaN when using Scratch gradients, which dont have set-stops
796
+ return createDraggable(s.c, isNaN(s.p) ? i * 100 : s.p)
797
+ });
798
+ draggableDiv.append(...newStops);
799
+
800
+ // preset values
801
+ settingsDiv.querySelector("select").value = settings.type;
802
+ settingsDiv.querySelector("input").value = settings.dir;
803
+ modalStorage.type = settings.type;
804
+ modalStorage.dir = settings.dir;
805
+ }
806
+
807
+ function handleFillEvent() {
808
+ if (!modalStorage._gradCache) return;
809
+
810
+ // set the swatch color in case the GUI resets it
811
+ const swatch = document.querySelector(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
812
+ if (swatch) queueMicrotask(() => {
813
+ if (!modalStorage._gradCache) return;
814
+ swatch.style.background = encodeGradHTML(modalStorage._gradCache.settings);
815
+ });
816
+
817
+ const tool = paper.tool;
818
+ if (typeof tool?._getFillItem !== "function") return;
819
+
820
+ const item = tool._getFillItem();
821
+ if (!item) return;
822
+
823
+ const bounds = item.bounds;
824
+ let origin, destination;
825
+ if (modalStorage.type === "Radial") {
826
+ origin = new paper.Point(tool._point.x, tool._point.y);
827
+ destination = origin.add([Math.max(bounds.width, bounds.height) / 2, 0]);
828
+ } else {
829
+ const points = genLinearGradPoints(bounds, modalStorage.dir - 90);
830
+ origin = points.origin;
831
+ destination = points.destination;
832
+ }
833
+
834
+ const path = tool.fillProperty === "fill" ? "fillColor" : "strokeColor";
835
+ item[path] = {
836
+ gradient: modalStorage._gradCache.gradient,
837
+ origin, destination
838
+ };
839
+ }
840
+
841
+ function handleShapeModeEvent(type) {
842
+ if (!modalStorage._gradCache && type !== "TEXT") return;
843
+
844
+ // set the swatch color in case the GUI resets it
845
+ const swatch = document.querySelector(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
846
+ if (swatch) queueMicrotask(() => {
847
+ if (!modalStorage._gradCache) return;
848
+ swatch.style.background = encodeGradHTML(modalStorage._gradCache.settings);
849
+ });
850
+
851
+ const tool = paper.tool;
852
+ if (typeof tool?._onMouseDrag !== "function") return;
853
+ if (tool[symbolTag]) return;
854
+ // patch this event, if not already, to run our code
855
+
856
+ const funcName = type === "TEXT" ? "onKeyDown" : "onMouseDrag";
857
+ const ogOnFunc = tool[funcName];
858
+ tool[symbolTag] = true;
859
+ tool[funcName] = function (...args) {
860
+ ogOnFunc.call(this, ...args);
861
+
862
+ // replace the fill with the custom gradient
863
+ if (!modalStorage._gradCache) {
864
+ if (type === "TEXT") {
865
+ tool.element.style.background = "";
866
+ tool.element.style.backgroundClip = "";
867
+ tool.element.style.color = "";
868
+ }
869
+ return;
870
+ }
871
+
872
+ let item;
873
+ switch (type) {
874
+ case "RECT":
875
+ item = this.rect;
876
+ break;
877
+ case "OVAL":
878
+ item = this.oval;
879
+ break;
880
+ case "TEXT":
881
+ item = this.textBox;
882
+ break;
883
+ /* PenguinMod shapes */
884
+ case "ROUNDED_RECT":
885
+ item = this.rect;
886
+ break;
887
+ case "TRIANGLE":
888
+ item = this.tri;
889
+ break;
890
+ case "SUSSY":
891
+ item = this.sussy;
892
+ break;
893
+ case "ARROW":
894
+ item = this.tri;
895
+ break;
896
+ default: return;
897
+ }
898
+ if (!item) return;
899
+ const bounds = item.bounds;
900
+ let origin, destination;
901
+ if (modalStorage.type === "Radial") {
902
+ origin = item.bounds.center;
903
+ destination = item.bounds.center.add([item.bounds.width / 2, 0]);
904
+ } else {
905
+ const points = genLinearGradPoints(bounds, modalStorage.dir - 90);
906
+ origin = points.origin;
907
+ destination = points.destination;
908
+ }
909
+
910
+ item.fillColor = {
911
+ gradient: modalStorage._gradCache.gradient,
912
+ origin, destination
913
+ };
914
+
915
+ // text uses HTML elements, so we have to handle that too
916
+ if (type === "TEXT") {
917
+ tool.element.style.background = encodeGradHTML(modalStorage._gradCache.settings);
918
+ tool.element.style.backgroundClip = "text";
919
+ tool.element.style.color = "transparent";
920
+ }
921
+ }
922
+ }
923
+
924
+ /* GUI Utils */
925
+ function getButtonURI(name, dontCompile) {
926
+ const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
927
+ const guiSVG = guiIMGS[name].replace("red", themeHex);
928
+ if (dontCompile) return guiSVG;
929
+ else return "data:image/svg+xml;base64," + btoa(guiSVG);
930
+ }
931
+
932
+ function showSelectedGrad(item) {
933
+ const [fillSwatch, outlineSwatch] = document.querySelectorAll(`div[class^=color-button_color-button_] div[class^=color-button_color-button-swatch_]`);
934
+ const outCSSGrad = paperGrad2CSS(extractGradient(item.strokeColor));
935
+ if (outlineSwatch) {
936
+ if (outCSSGrad) outlineSwatch.style.background = outCSSGrad;
937
+ else if (!item.strokeColor || item.strokeWidth === 0) outlineSwatch.style.background = "#fff";
938
+ }
939
+
940
+ const fillGrad = extractGradient(item.fillColor);
941
+ const fillCSSGrad = paperGrad2CSS(fillGrad);
942
+ modalStorage._gradCache = undefined;
943
+ if (fillSwatch) {
944
+ if (fillCSSGrad) {
945
+ fillSwatch.style.background = fillCSSGrad;
946
+
947
+ // update cache
948
+ const { gradient, destination, origin } = fillGrad;
949
+ modalStorage._gradCache = {
950
+ gradient,
951
+ settings: {
952
+ type: gradient.radial ? "Radial" : "Linear",
953
+ dir: position2Angle(destination, origin),
954
+ parts: gradient.stops.map(s => {
955
+ const alpha = Math.round(s.color.alpha * 255).toString(16).padStart(2, "0");
956
+ return { c: s.color.toCSS(true) + alpha, p: s.offset * 100 };
957
+ })
958
+ }
959
+ };
960
+ } else if (!item.fillColor) fillSwatch.style.background = "#fff";
961
+ }
962
+ }
963
+
964
+ function createDraggable(optC, optP) {
965
+ const index = modalStorage.parts.length;
966
+ const rngPos = optP ?? Math.floor(Math.random() * 100);
967
+ const rngHex = optC ?? `#${Math.floor(Math.random() * Math.pow(2, 24)).toString(16).padStart(6, "0")}`;
968
+ const opacity = optC ? optC.length === 9 ? parseInt(optC.slice(7, 9), 16) / 255 : 1 : 1;
969
+
970
+ const draggable = document.createElement("div");
971
+ draggable.id = index;
972
+ draggable.classList.add("pointer");
973
+ draggable.setAttribute("style", `cursor: pointer; width: 25px; position: absolute; top: -6px; transform: translateX(-50%);`);
974
+ draggable.style.left = `${rngPos}%`;
975
+
976
+ const nub = document.createElementNS("http://www.w3.org/2000/svg", "svg");
977
+ nub.setAttribute("width", "14");
978
+ nub.setAttribute("height", "7");
979
+ nub.style.transform = "translateX(45%)";
980
+
981
+ const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
982
+ polygon.setAttribute("points", "0,7 7,0 14,7");
983
+ polygon.setAttribute("stroke", "#fff");
984
+ polygon.setAttribute("fill", "#fff");
985
+ nub.appendChild(polygon);
986
+
987
+ const color = document.createElement("div");
988
+ color.setAttribute("style", `width: 25px; height: 25px; border-radius: 4px; background: #fff; display: flex; justify-content: center; align-items: center; flex-direction: column;`);
989
+
990
+ const colorContainer = document.createElement("div");
991
+ colorContainer.setAttribute("style", `width: 16px; height: 16px; border-radius: 5px; background: ${rngHex}; border: solid 2px rgba(0,0,0,.2); opacity: ${opacity}; margin-bottom: 2px;`);
992
+
993
+ const colorInput = document.createElement("input");
994
+ colorInput.setAttribute("type", "color");
995
+ colorInput.setAttribute("style", `opacity: 0; position: absolute; pointer-events: none;`);
996
+
997
+ const opacityInput = document.createElement("input");
998
+ opacityInput.setAttribute("type", "number");
999
+ opacityInput.setAttribute("min", "0");
1000
+ opacityInput.setAttribute("max", "100");
1001
+ opacityInput.value = opacity * 100;
1002
+ opacityInput.setAttribute("style", `visibility: hidden; background: #fff; border: none; color: #000; text-align: center; position: absolute; pointer-events: auto; width: 45px; height: 25px; padding: 0; margin: 0; border-radius: 0 5px 5px 0; left: 22px;`);
1003
+
1004
+ // Color picker handler
1005
+ colorContainer.addEventListener("click", (e) => {
1006
+ opacityInput.style.visibility = "visible";
1007
+ colorInput.click();
1008
+ e.stopPropagation();
1009
+ });
1010
+ draggable.addEventListener("mouseleave", (e) => {
1011
+ opacityInput.style.visibility = "hidden";
1012
+ e.stopPropagation();
1013
+ });
1014
+
1015
+ colorInput.addEventListener("input", (e) => {
1016
+ modalStorage.parts[index].c = e.target.value;
1017
+ colorContainer.style.background = e.target.value;
1018
+ updateDisplay();
1019
+ });
1020
+
1021
+ // Opacity slider handler
1022
+ opacityInput.addEventListener("click", (e) => {
1023
+ opacityInput.focus();
1024
+ e.stopPropagation();
1025
+ });
1026
+ opacityInput.addEventListener("input", (e) => {
1027
+ const newOpacity = Math.min(100, Math.max(0, e.target.value));
1028
+ e.target.value = newOpacity;
1029
+ colorContainer.style.opacity = newOpacity / 100;
1030
+
1031
+ const alpha = Math.round(newOpacity * 2.55).toString(16).padStart(2, "0");
1032
+ const hex = modalStorage.parts[index].c;
1033
+ modalStorage.parts[index].c = hex.substring(0, 7) + alpha;
1034
+ updateDisplay();
1035
+ });
1036
+
1037
+ draggable.addEventListener("mousedown", (e) => {
1038
+ e.preventDefault();
1039
+ if (e.target === opacityInput) return;
1040
+
1041
+ modalStorage.selectedPointer = draggable;
1042
+ const container = draggable.parentElement;
1043
+ const containerRect = container.getBoundingClientRect();
1044
+
1045
+ const onMouseMove = (moveEvent) => {
1046
+ const x = moveEvent.clientX - containerRect.left;
1047
+ const percent = Math.min(100, Math.max(0, (x / container.offsetWidth) * 100));
1048
+ draggable.style.left = `${percent}%`;
1049
+ modalStorage.parts[index].p = percent;
1050
+ updateDisplay();
1051
+ };
1052
+
1053
+ const onMouseUp = () => {
1054
+ document.removeEventListener("mousemove", onMouseMove);
1055
+ document.removeEventListener("mouseup", onMouseUp);
1056
+ };
1057
+
1058
+ document.addEventListener("mousemove", onMouseMove);
1059
+ document.addEventListener("mouseup", onMouseUp);
1060
+ });
1061
+
1062
+ color.append(colorContainer, colorInput, opacityInput);
1063
+ draggable.append(nub, color);
1064
+ modalStorage.parts.push({ c: rngHex, p: rngPos });
1065
+ modalStorage.selectedPointer = draggable;
1066
+ return draggable;
1067
+ }
1068
+
1069
+ function genSettingsTable(div) {
1070
+ const btnStyle = `width: 35px; height: 35px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--paint-input-background, --ui-primary, #fff); transition: transform 0.2s;`;
1071
+ const selectStlye = `cursor: pointer; height: 30px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--ui-secondary, #fff);`;
1072
+ const directionStyle = `text-align: center; width: 50px; height: 25px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: var(--ui-secondary, #fff);`;
1073
+
1074
+ const createBtn = document.createElement("button");
1075
+ createBtn.setAttribute("style", btnStyle);
1076
+ createBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
1077
+ createBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
1078
+ createBtn.innerHTML = getButtonURI("add", true);
1079
+ createBtn.addEventListener("click", (e) => {
1080
+ const draggableSpace = modalStorage.modal.querySelector(`div[class="draggables"]`);
1081
+ draggableSpace.appendChild(createDraggable());
1082
+ updateDisplay();
1083
+ e.stopPropagation();
1084
+ });
1085
+
1086
+ const deleteBtn = document.createElement("button");
1087
+ deleteBtn.setAttribute("style", btnStyle);
1088
+ deleteBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
1089
+ deleteBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
1090
+ deleteBtn.style.margin = "0px 8px";
1091
+ deleteBtn.innerHTML = getButtonURI("delete", true);
1092
+ deleteBtn.addEventListener("click", (e) => {
1093
+ const pointer = modalStorage.selectedPointer;
1094
+ if (pointer) {
1095
+ modalStorage.parts.splice(pointer.id, 1);
1096
+ pointer.remove();
1097
+ updateDisplay();
1098
+ delete modalStorage.selectedPointer;
1099
+ }
1100
+ e.stopPropagation();
1101
+ });
1102
+
1103
+ const title1 = document.createElement("span");
1104
+ title1.textContent = "Gradient Type:";
1105
+
1106
+ const select = document.createElement("select");
1107
+ select.setAttribute("style", selectStlye);
1108
+
1109
+ const option1 = document.createElement("option");
1110
+ const option2 = document.createElement("option");
1111
+ option1.text = "Linear"; option1.value = "Linear";
1112
+ option2.text = "Radial"; option2.value = "Radial";
1113
+ select.append(option1, option2);
1114
+ select.addEventListener("change", (e) => {
1115
+ modalStorage.type = e.target.value;
1116
+ updateDisplay();
1117
+ e.stopPropagation();
1118
+ });
1119
+
1120
+ const title2 = document.createElement("span");
1121
+ title2.textContent = "Direction:";
1122
+
1123
+ const dirBtn = document.createElement("input");
1124
+ dirBtn.setAttribute("style", directionStyle);
1125
+ dirBtn.setAttribute("type", "number");
1126
+ dirBtn.setAttribute("max", 360);
1127
+ dirBtn.setAttribute("min", 0);
1128
+ dirBtn.setAttribute("value", 90);
1129
+ dirBtn.addEventListener("input", (e) => {
1130
+ modalStorage.dir = e.target.value;
1131
+ updateDisplay();
1132
+ e.stopPropagation();
1133
+ });
1134
+
1135
+ div.append(createBtn, deleteBtn, title1, select, title2, dirBtn);
1136
+ }
1137
+
1138
+ function genButtonTable(div) {
1139
+ const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
1140
+ const btnStyle = `color: #fff; font-weight: 600; text-align: center; padding: 10px; margin: 10px 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; background: ${themeHex}; transition: transform 0.2s;`;
1141
+
1142
+ const enterBtn = document.createElement("button");
1143
+ enterBtn.id = "enter";
1144
+ enterBtn.setAttribute("style", btnStyle);
1145
+ enterBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
1146
+ enterBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
1147
+ enterBtn.textContent = "Okay";
1148
+
1149
+ const cancelBtn = document.createElement("button");
1150
+ cancelBtn.id = "cancel";
1151
+ cancelBtn.setAttribute("style", btnStyle);
1152
+ cancelBtn.setAttribute("onmouseover", `this.style.transform="scale(1.1)"`);
1153
+ cancelBtn.setAttribute("onmouseout", `this.style.transform="scale(1)"`);
1154
+ cancelBtn.textContent = "Cancel";
1155
+
1156
+ div.append(cancelBtn, enterBtn);
1157
+ }
1158
+
1159
+ function updateDisplay() {
1160
+ const display = modalStorage.modal.querySelector(`div[class="color-display"]`);
1161
+ display.style.background = encodeGradHTML(modalStorage);
1162
+ }
1163
+
1164
+ /* Main GUI */
1165
+ function openGradientMaker() {
1166
+ const paint = ReduxStore.getState().scratchPaint;
1167
+ const oldCache = modalStorage._gradCache;
1168
+ modalStorage = {
1169
+ parts: [], type: "Linear", dir: 90,
1170
+ selectedPointer: undefined, modal: undefined,
1171
+ path: paint.modals.fillColor ? "fillColor" : "strokeColor"
1172
+ };
1173
+
1174
+ const container = document.createElement("div");
1175
+ container.classList.add("SP-gradient-maker");
1176
+ container.setAttribute("style", `position: absolute; z-index: 9999; pointer-events: auto; background-color: rgba(0,0,0,.1); width: 100%; height: 100vh;`);
1177
+
1178
+ const modal = document.createElement("div");
1179
+ modal.classList.add("gradient-modal");
1180
+ modal.setAttribute("style", `color: var(--paint-text-primary, #575e75); width: 450px; height: 260px; display: block; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); background: var(--ui-secondary, hsla(215, 75%, 95%, 1)); border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px; padding: 15px;`);
1181
+ modalStorage.modal = modal;
1182
+
1183
+ const title = document.createElement("span");
1184
+ title.setAttribute("style", `display: block; text-align: center; justify-content: center; border-bottom: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); padding-bottom: 10px; margin: 0 25px 0 25px; font-weight: 600;"`);
1185
+ title.textContent = "Gradient Maker";
1186
+
1187
+ const display = document.createElement("div");
1188
+ display.classList.add("color-display");
1189
+ display.setAttribute("style", `width: 420px; height: 40px; display: flex; justify-content: center; align-items: center; margin: 15px 15px 0 15px; border: solid 2px grey; border-radius: 5px 5px 0 0;`);
1190
+
1191
+ const draggables = document.createElement("div");
1192
+ draggables.classList.add("draggables");
1193
+ draggables.setAttribute("style", `width: 420px; height: 40px; position: relative; display: flex; justify-content: center; align-items: center; margin: 0 15px 15px 15px; border: solid 2px grey; border-radius: 0 0 5px 5px; background: #111111;`);
1194
+
1195
+ const settings = document.createElement("div");
1196
+ settings.classList.add("settings");
1197
+ settings.setAttribute("style", `border-top: dashed 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); padding-top: 10px; display: flex; justify-content: center; align-items: center;`);
1198
+ genSettingsTable(settings);
1199
+
1200
+ const buttons = document.createElement("div");
1201
+ buttons.setAttribute("style", `display: flex; justify-content: center; align-items: center;`);
1202
+ genButtonTable(buttons);
1203
+ buttons.addEventListener("click", (e) => {
1204
+ if (e.target.id) {
1205
+ if (e.target.id === "enter") setSelected2Grad(modalStorage);
1206
+ container.remove();
1207
+ }
1208
+ e.stopPropagation();
1209
+ });
1210
+
1211
+ modal.append(title, display, draggables, settings, buttons);
1212
+ container.appendChild(modal);
1213
+ document.body.appendChild(container);
1214
+
1215
+ if (paint.selectedItems?.length) decodeSelectedGrad(paint.selectedItems[0], draggables, settings);
1216
+ else if (oldCache) decodeFromCache(oldCache.settings, draggables, settings);
1217
+ else draggables.append(createDraggable(), createDraggable());
1218
+ updateDisplay();
1219
+ }
1220
+
1221
+ function startListenerWorker() {
1222
+ let lastMode, lastSelected, lastModals;
1223
+ ReduxStore.subscribe(() => {
1224
+ const paint = ReduxStore.getState().scratchPaint;
1225
+ if (!paint || paint?.format === undefined || paint?.format === null) return;
1226
+ const { mode, selectedItems, modals } = paint;
1227
+
1228
+ // no bitmap support :(
1229
+ if (paint.format.startsWith("BITMAP")) {
1230
+ if (customBtn) {
1231
+ customBtn.remove();
1232
+ customBtn = undefined;
1233
+ }
1234
+ return;
1235
+ }
1236
+
1237
+ // run relative tool events
1238
+ if (mode === "FILL") handleFillEvent();
1239
+ else if (paperLinkModes.has(mode)) handleShapeModeEvent(mode);
1240
+
1241
+ const idChain = selectedItems.map((e) => e.id).join(".");
1242
+ const modalChain = `${modals.fillColor}${modals.strokeColor}`;
1243
+ if (mode === lastMode && idChain === lastSelected && modalChain === lastModals) return;
1244
+ lastMode = mode;
1245
+ lastSelected = idChain;
1246
+ lastModals = modalChain;
1247
+
1248
+ // decode potential custom gradients
1249
+ if (selectedItems?.length) showSelectedGrad(selectedItems[0]);
1250
+ else if (mode === "SELECT" || mode === "RESHAPE") modalStorage._gradCache = undefined;
1251
+
1252
+ // add custom modal
1253
+ if (!modals.strokeColor && !modals.fillColor) return;
1254
+ if (observerUsed) return;
1255
+ const observer = new MutationObserver(() => {
1256
+ const gradRow = document.querySelector(`div[class^="color-picker_gradient-picker-row_"]`);
1257
+ if (!gradRow || gradRow.lastElementChild.id === customID) return;
1258
+
1259
+ // get the appropriate class names for selected items
1260
+ if (!selectedClassName) initGradSelectClasses(gradRow);
1261
+ const children = Array.from(gradRow.children);
1262
+
1263
+ customBtn = children[0].cloneNode(true);
1264
+ customBtn.src = getButtonURI("select");
1265
+ customBtn.id = customID;
1266
+ customBtn.setAttribute("class", unselectedClassName);
1267
+ gradRow.appendChild(customBtn);
1268
+
1269
+ gradRow.addEventListener("click", (e) => {
1270
+ if (e.target === customBtn) {
1271
+ for (const child of children) child.setAttribute("class", unselectedClassName);
1272
+ customBtn.setAttribute("class", selectedClassName);
1273
+ openGradientMaker();
1274
+ } else if (e.target.nodeName === "IMG") {
1275
+ customBtn.setAttribute("class", unselectedClassName);
1276
+ }
1277
+ });
1278
+
1279
+ observerUsed = false;
1280
+ observer.disconnect();
1281
+ });
1282
+
1283
+ observer.observe(document.body, { childList: true, subtree: true });
1284
+ observerUsed = true;
1285
+ });
1286
+ }
1287
+
1288
+ if (typeof scaffolding === "undefined") startListenerWorker();
1289
+ }