soiz1 commited on
Commit
2412423
·
verified ·
1 Parent(s): 9c50c85

Create src/addons/addons/paint-tool-panel/userscript.js

Browse files
src/addons/addons/paint-tool-panel/userscript.js ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Costume Tool Panel
2
+ // By: SharkPool & DogeisCut
3
+ export default async function () {
4
+ const runtime = vm.runtime;
5
+ const isPM = true;
6
+
7
+ const panelTag = Symbol("costume-tools-tag");
8
+ const valueObserverObj = { r: 0, sx: 1, sy: 1, kx: 0, ky: 0, outlineRatio: 1 };
9
+ const panelID = "costume-editor-panel";
10
+ const epsilon = 1e-8;
11
+
12
+ const guiIMGS = {
13
+ "panel": `<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><rect x="10.796" y="6.222" width="4.813" height="2.174" rx=".5" ry=".5" fill="#fff" fill-rule="evenodd" stroke="#575e75" stroke-width=".75"/><path fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75" style="paint-order:markers stroke fill" d="M3 3h14v14H3z"/><g fill="#fff" fill-rule="evenodd" stroke-width=".75"><path stroke="red" stroke-linecap="round" stroke-linejoin="round" style="paint-order:markers fill stroke" d="M3 3h14v1.896H3z"/><rect x="10.796" y="9.722" width="4.813" height="2.174" rx=".5" ry=".5" stroke="#575e75"/><rect x="10.796" y="13.222" width="4.813" height="2.174" rx=".5" ry=".5" stroke="#575e75"/></g><path d="M9.563 7.309H4.242m5.321 6.978H4.242m5.321-3.478H4.242" style="paint-order:markers stroke fill" fill="none" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></svg>`,
14
+ "lock": `<svg xmlns="http://www.w3.org/2000/svg" width="38" height="40.98" viewBox="0 0 38 40.98"><path d="M19.086 0c7.566 0 13.903 5.707 13.903 12.912q0 .499-.046.987h1.083A3.98 3.98 0 0 1 38 17.879v19.125a3.98 3.98 0 0 1-3.974 3.976H3.977A3.98 3.98 0 0 1 0 37V17.875a3.98 3.98 0 0 1 3.98-3.976h24.974a8 8 0 0 0 .064-.987c0-4.863-4.333-8.937-9.931-8.937-3.266 0-6.127 1.409-7.922 3.53-.266.315-1.459 1.852-1.803 2.95-.568 1.815-.262 3.397-.262 3.397l-4.026.07S4.153 9.1 7.655 5.311C11.249 1.421 14.994 0 19.086 0" fill="red" fill-rule="evenodd"/></svg>`,
15
+ "unlock": `<svg xmlns="http://www.w3.org/2000/svg" width="38" height="40.98" viewBox="0 0 38 40.98"><path d="M18.913 0c4.412 0 8.388 1.907 10.956 4.948.705.84.598 2.093-.24 2.801a1.99 1.99 0 0 1-2.793-.244c-1.795-2.121-4.656-3.53-7.922-3.53-5.598 0-9.931 4.074-9.931 8.937q0 .495.064.987h24.974a3.98 3.98 0 0 1 3.98 3.976V37a3.98 3.98 0 0 1-3.976 3.98H3.972A3.98 3.98 0 0 1 0 37.004V17.879a3.98 3.98 0 0 1 3.972-3.98h1.083q-.045-.49-.045-.987C5.01 5.707 11.347 0 18.913 0" fill="red" fill-rule="evenodd"/></svg>`,
16
+ "exit": `<svg viewBox="-10 7 20 20" xmlns="http://www.w3.org/2000/svg"><path transform="rotate(45)" 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>`,
17
+ };
18
+ const blendingModes = [
19
+ "normal", "multiply", "screen", "overlay",
20
+ "soft-light", "hard-light",
21
+ "color-dodge", "color-burn",
22
+ "darken", "lighten",
23
+ "difference", "exclusion",
24
+ "hue", "saturation", "color", "luminosity"
25
+ ];
26
+ const outlineTypes = [
27
+ { text: "thorn", value: "miter" },
28
+ { text: "round", value: "round" },
29
+ { text: "bevel", value: "bevel" }
30
+ ];
31
+ const tools = [
32
+ {
33
+ title: "Position",
34
+ items: [
35
+ { title: "x", input: "number", params: "x" },
36
+ { title: "y", input: "number", params: "y" },
37
+ { title: "Group Position", id: "groupPosition", input: "toggler" },
38
+ ]
39
+ },
40
+ {
41
+ title: "Rotation",
42
+ items: [
43
+ { title: "direction", input: "number", params: "dir" },
44
+ "br",
45
+ { title: "Group Rotation", id: "groupRotate", input: "toggler" },
46
+ ]
47
+ },
48
+ {
49
+ title: "Scaling",
50
+ items: [
51
+ { title: "x", input: "number", params: "sx" },
52
+ { title: "y", input: "number", params: "sy" },
53
+ { title: "Group Scaling", id: "groupScale", input: "toggler" },
54
+ "br",
55
+ { title: "Scale Outlines", id: "strokeScale", input: "toggler" },
56
+ ]
57
+ },
58
+ {
59
+ title: "Skewing",
60
+ items: [
61
+ { title: "x", input: "number", params: "sx" },
62
+ { title: "y", input: "number", params: "sy" }
63
+ ]
64
+ },
65
+ {
66
+ title: "Layering", noBitmap: true,
67
+ items: [{ title: "order", input: "number", params: "layer" }]
68
+ },
69
+ {
70
+ title: "Blending", noBitmap: true,
71
+ items: [{ title: "mode", input: "select", params: "blend" }]
72
+ },
73
+ {
74
+ title: "Outlines", noBitmap: true,
75
+ items: [
76
+ { title: "type", input: "select", params: "outlineType" },
77
+ { title: "dash", input: "number", params: "outlineDash" }
78
+ ]
79
+ }
80
+ ];
81
+
82
+ let modalStorage = {};
83
+ let isPatched = false, sessionCenterNeedsUpdate = false, updatesAllowed = true;
84
+
85
+ /* Patches */
86
+ function applyPatches() {
87
+ // we need to store the original rotation, scale, and skew as these functions
88
+ // CHANGE, not SET, the items attributes
89
+ isPatched = true;
90
+ const ogRotate = paper.Item.prototype.rotate;
91
+ paper.Item.prototype.rotate = function(...args) {
92
+ const realAngle = typeof args[0] === "number" ? args[0] : 0;
93
+ for (const child of this._children ?? [this]) {
94
+ if (!child[panelTag]) child[panelTag] = structuredClone(valueObserverObj);
95
+ child[panelTag].r += realAngle;
96
+ }
97
+ return ogRotate.call(this, ...args);
98
+ };
99
+
100
+ /* Scratch-Paint uses Group.scale, not Item */
101
+ const ogScale = paper.Group.prototype.scale;
102
+ paper.Group.prototype.scale = function(...args) {
103
+ const sx = typeof args[0] === "number" ? args[0] : 1;
104
+ const sy = typeof args[1] === "number" ? args[1] : sx;
105
+ for (const child of this._children ?? [this]) {
106
+ if (!child[panelTag]) child[panelTag] = structuredClone(valueObserverObj);
107
+ child[panelTag].sx *= sx;
108
+ child[panelTag].sy *= sy;
109
+ }
110
+ return ogScale.call(this, ...args);
111
+ };
112
+
113
+ const ogSkew = paper.Item.prototype.skew;
114
+ paper.Item.prototype.skew = function(...args) {
115
+ const kx = typeof args[0] === "number" ? args[0] : 1;
116
+ const ky = typeof args[1] === "number" ? args[1] : kx;
117
+ for (const child of this._children ?? [this]) {
118
+ if (!child[panelTag]) child[panelTag] = structuredClone(valueObserverObj);
119
+ child[panelTag].kx += kx;
120
+ child[panelTag].ky += ky;
121
+ }
122
+ return ogSkew.call(this, ...args);
123
+ };
124
+ }
125
+
126
+ /* Internal Utils */
127
+ function _getSpecialLayer(layerString) {
128
+ for (const layer of paper.project.layers) {
129
+ if (layer.data && layer.data[layerString]) return layer;
130
+ }
131
+ }
132
+
133
+ function getRealSelection() {
134
+ return ReduxStore.getState().scratchPaint.selectedItems;
135
+ }
136
+
137
+ function apply2Selection(selectedItems = [], func, value) {
138
+ for (const item of selectedItems) func(item, value);
139
+
140
+ queueMicrotask(() => {
141
+ if (sessionCenterNeedsUpdate) {
142
+ // calculate the new center & clear session storage
143
+ modalStorage.sessionStore = { center: getCenter(selectedItems) };
144
+ sessionCenterNeedsUpdate = false;
145
+ }
146
+
147
+ const tool = paper.tool;
148
+ if (typeof tool.boundingBoxTool?.setSelectionBounds === "function") tool.boundingBoxTool.setSelectionBounds();
149
+ if (typeof tool.onUpdateImage === "function") {
150
+ // dash array is doubled when drawn on the stage, we need to half the value
151
+ // before sending to the stage
152
+ const changedItems = [];
153
+ for (const item of selectedItems) {
154
+ if (item.dashArray.length) {
155
+ changedItems.push(item);
156
+ item.dashArray[0] /= 2;
157
+ }
158
+ }
159
+ tool.onUpdateImage();
160
+ for (const item of changedItems) {
161
+ item.dashArray[0] *= 2;
162
+ }
163
+ }
164
+ });
165
+ }
166
+
167
+ function isGrouped(item) {
168
+ const parent = item.getParent();
169
+ if (parent.data && parent.data.isPaintingLayer) return false;
170
+ return true;
171
+ }
172
+
173
+ function getCenter(selectedItems) {
174
+ if (selectedItems.every(i => isGrouped(i))) {
175
+ const parent = selectedItems[0].getParent();
176
+ const bounds = parent.getBounds();
177
+ return [bounds.centerX, bounds.centerY];
178
+ } else {
179
+ const groupBounds = paper.Rectangle.create(selectedItems.map(i => i.bounds))
180
+ .reduce((a, b) => a.unite(b));
181
+ return [groupBounds.centerX, groupBounds.centerY];
182
+ }
183
+ }
184
+
185
+ function positionItem(item, value, isX, isLocked) {
186
+ const currentPos = item.getPosition();
187
+ if (isLocked) {
188
+ const session = modalStorage.sessionStore;
189
+ if (!session[item._id]) {
190
+ const center = session.center;
191
+ session[item._id] = [currentPos.x - center[0], currentPos.y - center[1]];
192
+ }
193
+
194
+ const offset = session[item._id];
195
+ if (isX) item.setPosition(value + offset[0], currentPos.y);
196
+ else item.setPosition(currentPos.x, value + offset[1]);
197
+ } else {
198
+ if (isX) item.setPosition(value, currentPos.y);
199
+ else item.setPosition(currentPos.x, value);
200
+ }
201
+ }
202
+
203
+ function rotateItem(item, degrees, isLocked) {
204
+ if (isLocked) {
205
+ item.rotate(degrees, modalStorage.sessionStore.center);
206
+ sessionCenterNeedsUpdate = true;
207
+ } else {
208
+ item.rotate(degrees);
209
+ }
210
+ }
211
+
212
+ function scaleItem(item, value, isX, isLocked, strokeUpdate) {
213
+ if (isLocked) {
214
+ if (isX) item.scale(value, 1, modalStorage.sessionStore.center);
215
+ else item.scale(1, value, modalStorage.sessionStore.center);
216
+ sessionCenterNeedsUpdate = true;
217
+ } else {
218
+ if (isX) item.scale(value, 1);
219
+ else item.scale(1, value);
220
+ }
221
+
222
+ if (strokeUpdate && item.strokeWidth) {
223
+ const meta = item[panelTag];
224
+ const newStrokeScale = Math.sqrt(meta.sx * meta.sy);
225
+
226
+ item.strokeWidth *= newStrokeScale / meta.outlineRatio;
227
+ meta.outlineRatio = newStrokeScale;
228
+ }
229
+ }
230
+
231
+ /* GUI Utils */
232
+ function getButtonURI(name, dontCompile) {
233
+ const themeHex = isPM ? "#00c3ff" : document.documentElement.style.getPropertyValue("--looks-secondary") || "#ff4c4c";
234
+ const guiSVG = guiIMGS[name].replaceAll("red", themeHex);
235
+ if (dontCompile) return guiSVG;
236
+ else return "data:image/svg+xml;base64," + btoa(guiSVG);
237
+ }
238
+
239
+ function getToolParams(name, selectedItems = []) {
240
+ const item1 = selectedItems[0];
241
+ const isX = name.endsWith("x");
242
+ switch (name) {
243
+ case "Position/x":
244
+ case "Position/y": {
245
+ if (selectedItems.length > 1) {
246
+ if (modalStorage["groupPosition"]) return {
247
+ max: 15000,
248
+ value: isX ? getCenter(selectedItems)[0] - runtime.stageWidth
249
+ : (getCenter(selectedItems)[1] - runtime.stageHeight) * -1
250
+ };
251
+ return { value: "", max: 15000, placeholder: "mixed" };
252
+ }
253
+ return {
254
+ max: 15000,
255
+ value: isX ? item1.getPosition().x - runtime.stageWidth
256
+ : (item1.getPosition().y - runtime.stageHeight) * -1
257
+ };
258
+ }
259
+ case "Rotation/direction": {
260
+ if (selectedItems.length > 1) return { min: 0, max: 360, value: "", placeholder: "mixed" };
261
+ else return {
262
+ min: 0, max: 360,
263
+ value: ((item1[panelTag]?.r ?? 0) + 90) % 180
264
+ };
265
+ }
266
+ case "Scaling/x":
267
+ case "Scaling/y": {
268
+ if (selectedItems.length > 1) return { value: "", placeholder: "mixed" };
269
+ else return {
270
+ max: 15000,
271
+ value: (item1[panelTag] ? isX ? item1[panelTag].sx : item1[panelTag].sy : 1) * 100
272
+ };
273
+ }
274
+ case "Skewing/x":
275
+ case "Skewing/y": {
276
+ if (selectedItems.length > 1) return { value: "", placeholder: "mixed" };
277
+ else return {
278
+ max: 15000,
279
+ value: item1[panelTag] ? isX ? item1[panelTag].kx : item1[panelTag].ky : 0
280
+ };
281
+ }
282
+ case "Layering/order": {
283
+ const children = _getSpecialLayer("isPaintingLayer").children;
284
+ if (selectedItems.length > 1) {
285
+ return { min: 1, max: children.length - 1, value: "", placeholder: "mixed" };
286
+ } else {
287
+ const index = children.length - children.indexOf(item1);
288
+ return {
289
+ min: 1, max: children.length - 1,
290
+ value: index - 1
291
+ };
292
+ }
293
+ }
294
+ case "Blending/mode": {
295
+ const options = [];
296
+ for (const mode of blendingModes) {
297
+ const option = document.createElement("option");
298
+ option.text = mode; option.value = mode;
299
+ options.push(option);
300
+ }
301
+ return { options, value: item1.blendMode };
302
+ }
303
+ case "Outlines/type": {
304
+ const options = [];
305
+ for (const mode of outlineTypes) {
306
+ const option = document.createElement("option");
307
+ option.text = mode.text; option.value = mode.value;
308
+ options.push(option);
309
+ }
310
+ return { options, value: item1.strokeJoin };
311
+ }
312
+ case "Outlines/dash": {
313
+ if (selectedItems.length > 1) return { min: 1, value: "", placeholder: "mixed" };
314
+ else return { min: 1, value: item1.dashArray[0] ?? 0 };
315
+ }
316
+ }
317
+ return {};
318
+ }
319
+
320
+ function getToolFunc(name) {
321
+ const isX = name.endsWith("x");
322
+ switch (name) {
323
+ case "Position/x":
324
+ case "Position/y": return (item, value) => {
325
+ value = parseFloat(value || 0) * (isX ? 1 : -1);
326
+ value += isX ? runtime.stageWidth : runtime.stageHeight;
327
+ positionItem(item, value, isX, modalStorage["groupPosition"]);
328
+ }
329
+ case "Rotation/direction": return (item, value) => {
330
+ value -= item[panelTag]?.r ?? 0;
331
+ rotateItem(item, value - 90, modalStorage["groupRotate"]);
332
+ }
333
+ case "Scaling/x":
334
+ case "Scaling/y": return (item, value) => {
335
+ if (!item[panelTag]) item[panelTag] = structuredClone(valueObserverObj);
336
+
337
+ value /= 100;
338
+ if (!value) value = epsilon;
339
+ if (isX) {
340
+ value /= item[panelTag].sx;
341
+ item[panelTag].sx *= value;
342
+ } else {
343
+ value /= item[panelTag].sy;
344
+ item[panelTag].sy *= value;
345
+ }
346
+
347
+ scaleItem(item, value, isX, modalStorage["groupScale"], modalStorage["strokeScale"]);
348
+ }
349
+ case "Skewing/x":
350
+ case "Skewing/y": return (item, value) => {
351
+ if (isX) {
352
+ value -= item[panelTag]?.kx ?? 0;
353
+ item.skew(value, 0);
354
+ } else {
355
+ value -= item[panelTag]?.ky ?? 0;
356
+ item.skew(0, value);
357
+ }
358
+ }
359
+ case "Layering/order": return (item, value) => {
360
+ const layer = _getSpecialLayer("isPaintingLayer");
361
+ const children = layer.children.slice();
362
+
363
+ if (item.parent === layer) item.remove();
364
+ layer.insertChild(children.length - value - 1, item);
365
+ }
366
+ case "Blending/mode": return (item, value) => item.setBlendMode(value);
367
+ case "Outlines/type": return (item, value) => item.setStrokeJoin(value);
368
+ case "Outlines/dash": return (item, value) => item.setDashArray([value]);
369
+ }
370
+ }
371
+
372
+ /* Main GUI */
373
+ function generateToolDisplay() {
374
+ if (!modalStorage.toolDiv) return;
375
+ modalStorage.id = paper.project.selectedItems.map(i => i.id).join(".");
376
+ modalStorage.toolDiv.innerHTML = ""; // reset
377
+
378
+ const titleStyle = `display: block; text-align: center; border-bottom: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); padding-bottom: 5px; margin: 0 25px 0 25px; font-size: 14px; font-weight: 600;`;
379
+ const subTitleStyle = `margin: 5px; font-size: 12px;`;
380
+ const inputDivStyle = `display: flex; flex-wrap: wrap; justify-content: center; align-items: center; padding: 5px 10px 5px 10px;`;
381
+ const inputStyle = `text-align: center; font-size: 13px; width: 60px; height: 25px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 15px;`;
382
+ const selectStyle = `text-align: center; font-size: 13px; height: 25px; margin: 5px; border: solid 2px var(--ui-black-transparent, hsla(0, 0%, 0%, 0.15)); border-radius: 5px;`;
383
+ const togglerStyle = `background: transparent; border: none; margin-left: 3px; padding: 5px 0 0 0;`;
384
+
385
+ const selectedItems = getRealSelection();
386
+ const center = getCenter(selectedItems);
387
+
388
+ const toolList = [];
389
+ for (const tool of tools) {
390
+ if (tool.noBitmap && modalStorage.format === "BITMAP") continue;
391
+ const div = document.createElement("div");
392
+ div.setAttribute("style", `border-bottom: dotted 2px grey; padding: 5px;`);
393
+
394
+ const title = document.createElement("span");
395
+ title.setAttribute("style", titleStyle);
396
+ title.textContent = tool.title;
397
+
398
+ const inputDiv = document.createElement("div");
399
+ inputDiv.setAttribute("style", inputDivStyle);
400
+ for (const item of tool.items) {
401
+ if (item === "br") {
402
+ const breaker = document.createElement("div");
403
+ breaker.setAttribute("style", `flex-basis: 100%; height: 0;`);
404
+ inputDiv.appendChild(breaker);
405
+ continue;
406
+ }
407
+
408
+ const subTitle = document.createElement("span");
409
+ subTitle.setAttribute("style", subTitleStyle);
410
+ subTitle.textContent = item.title;
411
+
412
+ const id = `${tool.title}/${item.title}`;
413
+ let input;
414
+ if (item.input === "select") {
415
+ input = document.createElement("select");
416
+ input.setAttribute("style", selectStyle);
417
+ input.id = id;
418
+
419
+ const param = getToolParams(id, selectedItems);
420
+ input.append(...param.options);
421
+ input.value = param.value;
422
+
423
+ input.addEventListener("change", (e) => {
424
+ apply2Selection(selectedItems, getToolFunc(id), e.target.value);
425
+ e.stopPropagation();
426
+ });
427
+ } else if (item.input === "toggler") {
428
+ input = document.createElement("button");
429
+ input.setAttribute("style", togglerStyle);
430
+ input.id = id;
431
+ if (!modalStorage[item.id]) input.style.filter = "saturate(0)";
432
+
433
+ const img = document.createElement("img");
434
+ img.src = getButtonURI(modalStorage[item.id] ? "lock" : "unlock");
435
+ img.setAttribute("width", 15);
436
+ input.appendChild(img);
437
+
438
+ input.addEventListener("click", (e) => {
439
+ const bool = !modalStorage[item.id];
440
+ modalStorage[item.id] = bool
441
+ input.style.filter = bool ? "saturate(1)" : "saturate(0)";
442
+ img.src = getButtonURI(bool ? "lock" : "unlock");
443
+ e.stopPropagation();
444
+ });
445
+ } else {
446
+ input = document.createElement("input");
447
+ input.setAttribute("style", inputStyle);
448
+ input.id = id;
449
+ input.type = item.input;
450
+
451
+ const params = getToolParams(id, selectedItems);
452
+ if (params.max) input.setAttribute("max", params.max);
453
+ if (params.min) input.setAttribute("min", params.min);
454
+ if (params.placeholder) input.setAttribute("placeholder", params.placeholder);
455
+ else input.setAttribute("placeholder", "");
456
+ input.setAttribute("value", params.value);
457
+
458
+ input.addEventListener("input", (e) => {
459
+ apply2Selection(selectedItems, getToolFunc(id), e.target.value);
460
+ e.stopPropagation();
461
+ });
462
+ }
463
+
464
+ inputDiv.append(subTitle, input);
465
+ }
466
+
467
+ div.append(title, inputDiv);
468
+ toolList.push(div);
469
+ }
470
+
471
+ modalStorage.toolDiv.append(...toolList);
472
+ modalStorage.sessionStore.center = center;
473
+ }
474
+
475
+ function updateToolValues() {
476
+ const selectedItems = getRealSelection();
477
+ const toolDiv = modalStorage.toolDiv;
478
+ if (!toolDiv) return;
479
+ for (const tool of tools) {
480
+ for (const item of tool.items) {
481
+ if (item === "br") continue;
482
+
483
+ const id = `${tool.title}/${item.title}`;
484
+ const input = toolDiv.querySelector(`input[id="${id}"], select[id="${id}"`);
485
+ if (input) {
486
+ const params = getToolParams(id, selectedItems);
487
+ if (params.value !== "") input.value = params.value;
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ function closeToolModal() {
494
+ const existingModal = document.querySelector(`div[class="costume-tool-modal"]`);
495
+ if (existingModal) existingModal.remove();
496
+ }
497
+
498
+ function openToolPanel() {
499
+ const paint = ReduxStore.getState().scratchPaint;
500
+ delete modalStorage.toolDiv;
501
+ modalStorage.format = paint.format.startsWith("BITMAP") ? "BITMAP" : "VECTOR";
502
+ modalStorage.sessionStore = {};
503
+
504
+ const modal = document.createElement("div");
505
+ modal.classList.add("costume-tool-modal");
506
+ modal.setAttribute("style", `color: var(--paint-text-primary, #575e75); width: 275px; height: 350px; z-index: 99999; 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;`);
507
+ modal.id = "draggable";
508
+
509
+ const title = document.createElement("span");
510
+ 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;`);
511
+ title.id = "draggable";
512
+ title.textContent = "Object Editor";
513
+
514
+ const closeBtn = document.createElement("button");
515
+ closeBtn.setAttribute("style", `position: absolute; top: 10px; right: 5px; transform: scale(1.2); filter: saturate(0); justify-content: center; transition: transform 0.2s, filter 0.2s; background: transparent; border: none;`);
516
+ const exitImg = document.createElement("img");
517
+ exitImg.src = getButtonURI("exit");
518
+ exitImg.setAttribute("width", 20);
519
+ closeBtn.appendChild(exitImg);
520
+ closeBtn.addEventListener("mouseover", () => {
521
+ closeBtn.style.transform = "scale(1.5)";
522
+ closeBtn.style.filter = "saturate(1)";
523
+ });
524
+ closeBtn.addEventListener("mouseout", () => {
525
+ closeBtn.style.transform = "scale(1.2)";
526
+ closeBtn.style.filter = "saturate(0)";
527
+ });
528
+ closeBtn.addEventListener("click", (e) => {
529
+ closeToolModal();
530
+ e.stopPropagation();
531
+ });
532
+
533
+ const toolDisplay = document.createElement("div");
534
+ toolDisplay.id = "tool-display";
535
+ toolDisplay.setAttribute("style", `display: block; overflow-y: scroll;width: auto;height: 300px; margin-top: 15px;background: var(--ui-white);border: solid 2px grey;border-radius: 5px;`);
536
+ modalStorage.toolDiv = toolDisplay;
537
+ generateToolDisplay();
538
+
539
+ modal.append(title, closeBtn, toolDisplay);
540
+ document.body.appendChild(modal);
541
+
542
+ // value auto-updates
543
+ modal.addEventListener("mouseenter", () => { updatesAllowed = false });
544
+ modal.addEventListener("mouseleave", () => { updatesAllowed = true });
545
+
546
+ // drag n drop behaviour
547
+ modal.addEventListener("mousedown", (e) => {
548
+ if (e.target.id !== "draggable") return;
549
+ e.preventDefault();
550
+
551
+ const offsetX = e.clientX - modal.offsetLeft;
552
+ const offsetY = e.clientY - modal.offsetTop;
553
+
554
+ const onMouseMove = (moveEvent) => {
555
+ const newLeft = moveEvent.clientX - offsetX;
556
+ const newTop = moveEvent.clientY - offsetY;
557
+ modal.style.left = `${newLeft}px`;
558
+ modal.style.top = `${newTop}px`;
559
+ modal.style.boxShadow = "black 5px 5px 15px";
560
+ };
561
+
562
+ const onMouseUp = () => {
563
+ document.removeEventListener("mousemove", onMouseMove);
564
+ document.removeEventListener("mouseup", onMouseUp);
565
+ modal.style.boxShadow = "";
566
+ };
567
+
568
+ document.addEventListener("mousemove", onMouseMove);
569
+ document.addEventListener("mouseup", onMouseUp);
570
+ });
571
+ }
572
+
573
+ function startListenerWorker() {
574
+ ReduxStore.subscribe((e,t) => {
575
+ const paint = ReduxStore.getState().scratchPaint;
576
+ if (!paint) return;
577
+
578
+ // add panel button
579
+ const editorRow = document.querySelector(`div[class^="paint-editor_row_"] div[class^="fixed-tools_row_"]`);
580
+ if (!editorRow) return;
581
+ if (!isPatched) applyPatches();
582
+
583
+ // check if button exists
584
+ let panelBtn = editorRow.querySelector(`div[id="${panelID}"]`);
585
+ if (!panelBtn) {
586
+ let groupTool = editorRow.querySelectorAll(`div[class*="input-group_input-group_"]`);
587
+ groupTool = groupTool[2] ?? groupTool[1];
588
+
589
+ panelBtn = document.createElement("div");
590
+ panelBtn.setAttribute("style", `border-right: 1px dashed var(--paint-ui-pane-border, #D9D9D9); margin-right: calc(2 * .25rem); display: inline-block; padding: .25rem .325rem; min-width: 3rem; text-align: center;`);
591
+ panelBtn.id = panelID;
592
+
593
+ const container = document.createElement("span");
594
+ const img = document.createElement("img");
595
+ img.setAttribute("draggable", false);
596
+ img.setAttribute("style", `width: 1.5rem; vertical-align: middle;`);
597
+
598
+ const label = document.createElement("span");
599
+ label.setAttribute("style", `display: block; margin-top: .125rem; font-size: .625rem;`);
600
+ label.textContent = "Tools";
601
+
602
+ container.append(img, label);
603
+ panelBtn.appendChild(container);
604
+ groupTool.insertAdjacentElement("afterend", panelBtn);
605
+
606
+ panelBtn.children[0].addEventListener("click", (e) => {
607
+ if (paper.project.selectedItems.length) {
608
+ panelBtn.style.opacity = "1";
609
+ closeToolModal();
610
+ openToolPanel();
611
+ }
612
+ e.stopPropagation();
613
+ });
614
+ }
615
+
616
+ queueMicrotask(() => {
617
+ const hasSelection = paper.project && paper?.project?.selectedItems.length > 0;
618
+ panelBtn.children[0].children[0].src = getButtonURI("panel");
619
+ panelBtn.children[0].setAttribute("style", `opacity: ${hasSelection ? 1 : .5}; cursor: pointer;`);
620
+ if (!hasSelection) closeToolModal();
621
+ else if (updatesAllowed) updateToolValues();
622
+ });
623
+ });
624
+ }
625
+
626
+ if (typeof scaffolding === "undefined") startListenerWorker();
627
+ }