293 lines
9.8 KiB
React
293 lines
9.8 KiB
React
|
|
/**
|
||
|
|
* generate_inner_shadow_dataset.jsx
|
||
|
|
*
|
||
|
|
* Purpose:
|
||
|
|
* Generate a systematic Inner Shadow effect dataset in Photopea.
|
||
|
|
* Uses stringID-based ActionDescriptor for maximum Photopea compatibility.
|
||
|
|
*
|
||
|
|
* Logic & Workflow:
|
||
|
|
* 1. Creates a new RGB document with transparent background.
|
||
|
|
* 2. Adds a normal raster layer and fills the center with black.
|
||
|
|
* 3. Applies one Inner Shadow effect via ActionDescriptor (stringID).
|
||
|
|
* 4. Hides the background layer.
|
||
|
|
* 5. Saves the result as a transparent PNG.
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* 1. Open https://www.photopea.com in a browser.
|
||
|
|
* 2. Alt+Ctrl+I (or File > Automate > Script).
|
||
|
|
* 3. Paste this script and run.
|
||
|
|
* 4. Select output folder when prompted.
|
||
|
|
*/
|
||
|
|
|
||
|
|
var outFolder = Folder.selectDialog("Select output folder for Inner Shadow dataset");
|
||
|
|
if (!outFolder) {
|
||
|
|
alert("No output folder selected. Exiting.");
|
||
|
|
throw new Error("No output folder");
|
||
|
|
}
|
||
|
|
|
||
|
|
var angles = [0, 90, 180, 270];
|
||
|
|
var distances = [5, 15, 30];
|
||
|
|
var sizes = [10, 30];
|
||
|
|
var chokes = [0, 20];
|
||
|
|
var fxOpacities = [75, 100];
|
||
|
|
var fxBlendModes = [
|
||
|
|
{ id: "normal", name: "normal" },
|
||
|
|
{ id: "multiply", name: "multiply" }
|
||
|
|
];
|
||
|
|
|
||
|
|
function createDocument(name, width, height) {
|
||
|
|
var doc = app.documents.add(width, height, 72, name, NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
|
||
|
|
try {
|
||
|
|
var bg = doc.artLayers[doc.artLayers.length - 1];
|
||
|
|
if (bg.isBackgroundLayer) {
|
||
|
|
bg.visible = false;
|
||
|
|
}
|
||
|
|
} catch (e) {}
|
||
|
|
return doc;
|
||
|
|
}
|
||
|
|
|
||
|
|
function addBlackRectangle(doc) {
|
||
|
|
var w = doc.width;
|
||
|
|
var h = doc.height;
|
||
|
|
if (typeof w === "object" && typeof w.value === "function") { w = w.value; }
|
||
|
|
if (typeof h === "object" && typeof h.value === "function") { h = h.value; }
|
||
|
|
w = Number(w);
|
||
|
|
h = Number(h);
|
||
|
|
|
||
|
|
var margin = Math.round(Math.min(w, h) * 0.25);
|
||
|
|
|
||
|
|
var layer = doc.artLayers.add();
|
||
|
|
layer.name = "shape";
|
||
|
|
|
||
|
|
var region = [
|
||
|
|
[margin, margin],
|
||
|
|
[w - margin, margin],
|
||
|
|
[w - margin, h - margin],
|
||
|
|
[margin, h - margin]
|
||
|
|
];
|
||
|
|
|
||
|
|
doc.selection.select(region);
|
||
|
|
|
||
|
|
var black = new SolidColor();
|
||
|
|
black.rgb.red = 0;
|
||
|
|
black.rgb.green = 0;
|
||
|
|
black.rgb.blue = 0;
|
||
|
|
|
||
|
|
doc.selection.fill(black);
|
||
|
|
doc.selection.deselect();
|
||
|
|
|
||
|
|
return layer;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Try applying Inner Shadow via ActionDescriptor with stringID keys.
|
||
|
|
* Returns true on success, false on failure.
|
||
|
|
*/
|
||
|
|
function applyInnerShadow(params) {
|
||
|
|
try {
|
||
|
|
var desc = new ActionDescriptor();
|
||
|
|
|
||
|
|
var ref = new ActionReference();
|
||
|
|
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID("layerEffects"));
|
||
|
|
ref.putEnumerated(stringIDToTypeID("layer"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
|
||
|
|
desc.putReference(stringIDToTypeID("null"), ref);
|
||
|
|
|
||
|
|
var fxDesc = new ActionDescriptor();
|
||
|
|
|
||
|
|
var isDesc = new ActionDescriptor();
|
||
|
|
isDesc.putBoolean(stringIDToTypeID("enabled"), true);
|
||
|
|
|
||
|
|
// Blend mode
|
||
|
|
isDesc.putEnumerated(stringIDToTypeID("mode"), stringIDToTypeID("blendMode"), stringIDToTypeID(params.blendMode));
|
||
|
|
|
||
|
|
// Color
|
||
|
|
var colorDesc = new ActionDescriptor();
|
||
|
|
colorDesc.putDouble(stringIDToTypeID("red"), 0);
|
||
|
|
colorDesc.putDouble(stringIDToTypeID("grain"), 0);
|
||
|
|
colorDesc.putDouble(stringIDToTypeID("blue"), 0);
|
||
|
|
isDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("RGBColor"), colorDesc);
|
||
|
|
|
||
|
|
// Opacity (0-100)
|
||
|
|
isDesc.putInteger(stringIDToTypeID("opacity"), params.opacity);
|
||
|
|
|
||
|
|
// Angle
|
||
|
|
isDesc.putInteger(stringIDToTypeID("localLightingAngle"), params.angle);
|
||
|
|
|
||
|
|
// Global light
|
||
|
|
isDesc.putBoolean(stringIDToTypeID("useGlobalAngle"), false);
|
||
|
|
|
||
|
|
// Distance
|
||
|
|
isDesc.putInteger(stringIDToTypeID("distance"), params.distance);
|
||
|
|
|
||
|
|
// Choke
|
||
|
|
isDesc.putInteger(stringIDToTypeID("chokeMatte"), params.choke);
|
||
|
|
|
||
|
|
// Blur (size)
|
||
|
|
isDesc.putInteger(stringIDToTypeID("blur"), params.size);
|
||
|
|
|
||
|
|
// Noise
|
||
|
|
isDesc.putInteger(stringIDToTypeID("noise"), 0);
|
||
|
|
|
||
|
|
// Anti-alias
|
||
|
|
isDesc.putBoolean(stringIDToTypeID("antiAlias"), true);
|
||
|
|
|
||
|
|
// Transfer function (contour)
|
||
|
|
isDesc.putBoolean(stringIDToTypeID("transferSpecIsScaled"), false);
|
||
|
|
|
||
|
|
fxDesc.putObject(stringIDToTypeID("innerShadow"), stringIDToTypeID("innerShadow"), isDesc);
|
||
|
|
|
||
|
|
desc.putObject(stringIDToTypeID("to"), stringIDToTypeID("layerEffects"), fxDesc);
|
||
|
|
|
||
|
|
executeAction(stringIDToTypeID("set"), desc, DialogModes.NO);
|
||
|
|
return true;
|
||
|
|
} catch (e) {
|
||
|
|
$.writeln("stringID failed: " + e);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fallback: apply via charID-based descriptor.
|
||
|
|
*/
|
||
|
|
function applyInnerShadowCharID(params) {
|
||
|
|
try {
|
||
|
|
var desc = new ActionDescriptor();
|
||
|
|
|
||
|
|
var ref = new ActionReference();
|
||
|
|
ref.putProperty(charIDToTypeID("Prpr"), charIDToTypeID("Lefx"));
|
||
|
|
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
|
||
|
|
desc.putReference(charIDToTypeID("null"), ref);
|
||
|
|
|
||
|
|
var fxDesc = new ActionDescriptor();
|
||
|
|
|
||
|
|
var isDesc = new ActionDescriptor();
|
||
|
|
isDesc.putBoolean(charIDToTypeID("enab"), true);
|
||
|
|
isDesc.putBoolean(charIDToTypeID("present"), true);
|
||
|
|
isDesc.putBoolean(charIDToTypeID("showInDialog"), true);
|
||
|
|
|
||
|
|
// Blend mode
|
||
|
|
isDesc.putEnumerated(charIDToTypeID("Blnd"), charIDToTypeID("BlnM"), charIDToTypeID(params.blendModeCharID));
|
||
|
|
|
||
|
|
// Color
|
||
|
|
var clr = new ActionDescriptor();
|
||
|
|
clr.putDouble(charIDToTypeID("Rd "), 0);
|
||
|
|
clr.putDouble(charIDToTypeID("Grn "), 0);
|
||
|
|
clr.putDouble(charIDToTypeID("Bl "), 0);
|
||
|
|
isDesc.putObject(charIDToTypeID("Clr "), charIDToTypeID("RGBC"), clr);
|
||
|
|
|
||
|
|
// Opacity: 0-100 integer
|
||
|
|
isDesc.putInteger(charIDToTypeID("Opct"), params.opacity);
|
||
|
|
|
||
|
|
// Angle: use lagl (local angle) instead of Angl
|
||
|
|
isDesc.putInteger(charIDToTypeID("lagl"), params.angle);
|
||
|
|
isDesc.putBoolean(charIDToTypeID("uglg"), false);
|
||
|
|
|
||
|
|
// Distance
|
||
|
|
isDesc.putInteger(charIDToTypeID("Dstn"), params.distance);
|
||
|
|
|
||
|
|
// Choke
|
||
|
|
isDesc.putInteger(charIDToTypeID("Ckmt"), params.choke);
|
||
|
|
|
||
|
|
// Blur
|
||
|
|
isDesc.putInteger(charIDToTypeID("blur"), params.size);
|
||
|
|
|
||
|
|
// Noise
|
||
|
|
isDesc.putInteger(charIDToTypeID("Nose"), 0);
|
||
|
|
|
||
|
|
fxDesc.putObject(charIDToTypeID("IrSh"), charIDToTypeID("IrSh"), isDesc);
|
||
|
|
|
||
|
|
desc.putObject(charIDToTypeID("T "), charIDToTypeID("Lefx"), fxDesc);
|
||
|
|
|
||
|
|
executeAction(charIDToTypeID("setd"), desc, DialogModes.NO);
|
||
|
|
return true;
|
||
|
|
} catch (e) {
|
||
|
|
$.writeln("charID failed: " + e);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function saveTransparentPng(doc, file) {
|
||
|
|
var pngOptions = new PNGSaveOptions();
|
||
|
|
pngOptions.compression = 6;
|
||
|
|
pngOptions.interlaced = false;
|
||
|
|
doc.saveAs(file, pngOptions, true, Extension.LOWERCASE);
|
||
|
|
}
|
||
|
|
|
||
|
|
var count = 0;
|
||
|
|
var errors = 0;
|
||
|
|
var total = angles.length * distances.length * sizes.length * chokes.length * fxOpacities.length * fxBlendModes.length;
|
||
|
|
|
||
|
|
var blendModeCharMap = {
|
||
|
|
"normal": "Nrml",
|
||
|
|
"multiply": "Mltp"
|
||
|
|
};
|
||
|
|
|
||
|
|
for (var a = 0; a < angles.length; a++) {
|
||
|
|
for (var d = 0; d < distances.length; d++) {
|
||
|
|
for (var s = 0; s < sizes.length; s++) {
|
||
|
|
for (var c = 0; c < chokes.length; c++) {
|
||
|
|
for (var o = 0; o < fxOpacities.length; o++) {
|
||
|
|
for (var b = 0; b < fxBlendModes.length; b++) {
|
||
|
|
var bmName = fxBlendModes[b].name;
|
||
|
|
var bmID = fxBlendModes[b].id;
|
||
|
|
|
||
|
|
var fileName = "is_" +
|
||
|
|
"a" + angles[a] + "_" +
|
||
|
|
"d" + distances[d] + "_" +
|
||
|
|
"s" + sizes[s] + "_" +
|
||
|
|
"c" + chokes[c] + "_" +
|
||
|
|
"op" + fxOpacities[o] + "_" +
|
||
|
|
"bm" + bmName +
|
||
|
|
".png";
|
||
|
|
|
||
|
|
var file = new File(outFolder + "/" + fileName);
|
||
|
|
if (file.exists) {
|
||
|
|
count++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var params = {
|
||
|
|
angle: angles[a],
|
||
|
|
distance: distances[d],
|
||
|
|
size: sizes[s],
|
||
|
|
choke: chokes[c],
|
||
|
|
opacity: fxOpacities[o],
|
||
|
|
blendMode: bmID,
|
||
|
|
blendModeCharID: blendModeCharMap[bmID] || "Nrml"
|
||
|
|
};
|
||
|
|
|
||
|
|
var doc = createDocument("inner_shadow_test", 256, 256);
|
||
|
|
var layer = addBlackRectangle(doc);
|
||
|
|
|
||
|
|
var applied = applyInnerShadow(params);
|
||
|
|
if (!applied) {
|
||
|
|
applied = applyInnerShadowCharID(params);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (applied) {
|
||
|
|
try {
|
||
|
|
saveTransparentPng(doc, file);
|
||
|
|
} catch (e2) {
|
||
|
|
$.writeln("Save error: " + fileName + " — " + e2);
|
||
|
|
errors++;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$.writeln("FAILED to apply effect: " + fileName);
|
||
|
|
errors++;
|
||
|
|
}
|
||
|
|
|
||
|
|
doc.close(SaveOptions.DONOTSAVECHANGES);
|
||
|
|
|
||
|
|
count++;
|
||
|
|
if (count % 10 === 0) {
|
||
|
|
$.writeln("Progress: " + count + " / " + total + " (errors: " + errors + ")");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
alert("Done. Generated " + count + " images, " + errors + " errors.\n" + outFolder.fsName);
|