#target illustrator /* Neon Scribble Generator for Adobe Illustrator Creates an editable, reference-style composition of glowing random paths. Run from Illustrator using: File > Scripts > Other Script... The script works on the active artboard. If no document is open, it creates a new 1920 x 1080 RGB document first. */ (function () { var SCRIPT_NAME = "Neon Scribble Generator"; var randomState = null; function rgb(red, green, blue) { var colour = new RGBColor(); colour.red = red; colour.green = green; colour.blue = blue; return colour; } function randomBetween(minimum, maximum) { return minimum + randomUnit() * (maximum - minimum); } function randomInteger(minimum, maximum) { return Math.floor(randomBetween(minimum, maximum + 1)); } function clamp(value, minimum, maximum) { return Math.max(minimum, Math.min(maximum, value)); } function setRandomSeed(seed) { if (seed === null) { randomState = null; return; } randomState = Math.abs(Math.floor(seed)) % 4294967296; if (randomState === 0) { randomState = 123456789; } } function randomUnit() { if (randomState === null) { return Math.random(); } randomState = (1664525 * randomState + 1013904223) % 4294967296; return randomState / 4294967296; } function chooseColour(palette) { return palette[randomInteger(0, palette.length - 1)]; } function copyColour(source) { return rgb(source.red, source.green, source.blue); } function colourFromHex(value) { var cleaned = value.replace(/\s/g, "").replace(/^#/, ""); if (/^[0-9a-fA-F]{3}$/.test(cleaned)) { cleaned = cleaned.charAt(0) + cleaned.charAt(0) + cleaned.charAt(1) + cleaned.charAt(1) + cleaned.charAt(2) + cleaned.charAt(2); } if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) { return null; } var numeric = parseInt(cleaned, 16); return rgb((numeric >> 16) & 255, (numeric >> 8) & 255, numeric & 255); } function lineStyleSettings(styleIndex) { if (styleIndex === 0) { return {turn: 0.25, jitter: 0.34, cornerChance: 0.02, handleMin: 0.18, handleMax: 0.25}; } if (styleIndex === 2) { return {turn: 0.72, jitter: 1.00, cornerChance: 0.55, handleMin: 0.04, handleMax: 0.11}; } return {turn: 0.46, jitter: 0.68, cornerChance: 0.12, handleMin: 0.115, handleMax: 0.205}; } function makeRandomAnchors(bounds, rightHeavy, minimumLength, maximumLength, styleIndex) { var left = bounds[0]; var top = bounds[1]; var right = bounds[2]; var bottom = bounds[3]; var width = right - left; var height = top - bottom; var shortSide = Math.min(width, height); var style = lineStyleSettings(styleIndex); var startDistribution = rightHeavy ? Math.pow(randomUnit(), 0.55) : randomUnit(); var x = left + width * startDistribution; var y = bottom + height * randomUnit(); var heading = randomBetween(-Math.PI, Math.PI); var turnBias = randomBetween(-style.turn, style.turn); var anchors = [[x, y]]; var outerLeft = left - width * 0.16; var outerRight = right + width * 0.16; var outerTop = top + height * 0.16; var outerBottom = bottom - height * 0.16; var targetX = left + width * (rightHeavy ? 0.67 : 0.50); var targetY = bottom + height * 0.50; var targetLength = width * randomBetween(minimumLength, maximumLength) / 100; var travelled = 0; var maximumPoints = 60; while ((travelled < targetLength || anchors.length < 4) && anchors.length < maximumPoints) { var step = randomBetween(shortSide * 0.075, shortSide * 0.22); var jitter = randomBetween(-style.jitter, style.jitter); heading += turnBias + jitter; var nearOutside = x < outerLeft || x > outerRight || y < outerBottom || y > outerTop; if (nearOutside) { heading = Math.atan2(targetY - y, targetX - x) + randomBetween(-0.55, 0.55); turnBias *= 0.65; } var previousX = x; var previousY = y; x += Math.cos(heading) * step; y += Math.sin(heading) * step; x = clamp(x, outerLeft - width * 0.08, outerRight + width * 0.08); y = clamp(y, outerBottom - height * 0.08, outerTop + height * 0.08); anchors.push([x, y]); travelled += Math.sqrt(Math.pow(x - previousX, 2) + Math.pow(y - previousY, 2)); if (randomUnit() < 0.13) { turnBias = randomBetween(-style.turn * 1.25, style.turn * 1.25); } } return anchors; } function buildPath(container, anchors, colour, strokeWidth, opacity, name, styleIndex) { var style = lineStyleSettings(styleIndex); var path = container.pathItems.add(); path.name = name; path.filled = false; path.stroked = true; path.closed = false; path.strokeColor = copyColour(colour); path.strokeWidth = strokeWidth; path.opacity = opacity; path.strokeCap = StrokeCap.ROUNDENDCAP; path.strokeJoin = StrokeJoin.ROUNDENDJOIN; try { path.blendingMode = BlendModes.SCREEN; } catch (blendError) { // Older versions still generate the artwork, using Normal mode. } for (var index = 0; index < anchors.length; index++) { var anchor = anchors[index]; var previous = anchors[index === 0 ? 0 : index - 1]; var next = anchors[index === anchors.length - 1 ? anchors.length - 1 : index + 1]; var point = path.pathPoints.add(); point.anchor = anchor; // Catmull-Rom-style handles create smooth but unpredictable curves. var handleScale = randomBetween(style.handleMin, style.handleMax); var handleX = (next[0] - previous[0]) * handleScale; var handleY = (next[1] - previous[1]) * handleScale; // Occasional corners stop the result looking mechanically perfect. if (index > 0 && index < anchors.length - 1 && randomUnit() < style.cornerChance) { point.leftDirection = anchor; point.rightDirection = anchor; point.pointType = PointType.CORNER; } else { point.leftDirection = [anchor[0] - handleX, anchor[1] - handleY]; point.rightDirection = [anchor[0] + handleX, anchor[1] + handleY]; point.pointType = PointType.SMOOTH; } } return path; } function addLabel(group, text, size) { var label = group.add("statictext", undefined, text); label.preferredSize.width = size || 115; return label; } function showSettings() { var dialog = new Window("dialog", SCRIPT_NAME); dialog.orientation = "column"; dialog.alignChildren = ["fill", "top"]; dialog.spacing = 10; dialog.margins = 16; var panel = dialog.add("panel", undefined, "Generator settings"); panel.orientation = "column"; panel.alignChildren = ["fill", "top"]; panel.spacing = 9; panel.margins = 14; var countGroup = panel.add("group"); countGroup.orientation = "row"; addLabel(countGroup, "Number of lines:", 120); var countInput = countGroup.add("edittext", undefined, "85"); countInput.characters = 6; var densityGroup = panel.add("group"); densityGroup.orientation = "row"; addLabel(densityGroup, "Distribution:", 120); var densityList = densityGroup.add("dropdownlist", undefined, [ "Right-heavy, like reference", "Even across artboard" ]); densityList.selection = 0; densityList.preferredSize.width = 205; var colourGroup = panel.add("group"); colourGroup.orientation = "row"; addLabel(colourGroup, "Line colours:", 120); var colourInput = colourGroup.add("edittext", undefined, "#1852DC, #2674FF, #00AEFF, #841EFF, #FF8546"); colourInput.characters = 38; var colourNoteGroup = panel.add("group"); colourNoteGroup.orientation = "row"; addLabel(colourNoteGroup, "", 120); colourNoteGroup.add("statictext", undefined, "Separate hex colours with commas."); var styleGroup = panel.add("group"); styleGroup.orientation = "row"; addLabel(styleGroup, "Line character:", 120); var styleList = styleGroup.add("dropdownlist", undefined, [ "Smooth and flowing", "Organic and mixed", "Angular and chaotic" ]); styleList.selection = 1; styleList.preferredSize.width = 205; var strokeGroup = panel.add("group"); strokeGroup.orientation = "row"; addLabel(strokeGroup, "Stroke width:", 120); var minimumStrokeInput = strokeGroup.add("edittext", undefined, "0.55"); minimumStrokeInput.characters = 5; strokeGroup.add("statictext", undefined, "to"); var maximumStrokeInput = strokeGroup.add("edittext", undefined, "1.65"); maximumStrokeInput.characters = 5; strokeGroup.add("statictext", undefined, "pt"); var opacityGroup = panel.add("group"); opacityGroup.orientation = "row"; addLabel(opacityGroup, "Line opacity:", 120); var minimumOpacityInput = opacityGroup.add("edittext", undefined, "18"); minimumOpacityInput.characters = 5; opacityGroup.add("statictext", undefined, "to"); var maximumOpacityInput = opacityGroup.add("edittext", undefined, "92"); maximumOpacityInput.characters = 5; opacityGroup.add("statictext", undefined, "%"); var lengthGroup = panel.add("group"); lengthGroup.orientation = "row"; addLabel(lengthGroup, "Approx. length:", 120); var minimumLengthInput = lengthGroup.add("edittext", undefined, "40"); minimumLengthInput.characters = 5; lengthGroup.add("statictext", undefined, "to"); var maximumLengthInput = lengthGroup.add("edittext", undefined, "130"); maximumLengthInput.characters = 5; lengthGroup.add("statictext", undefined, "% of artboard width"); var glowGroupSettings = panel.add("group"); glowGroupSettings.orientation = "row"; var glowCheckbox = glowGroupSettings.add("checkbox", undefined, "Add neon glow"); glowCheckbox.value = true; glowGroupSettings.add("statictext", undefined, "Strength:"); var glowStrengthList = glowGroupSettings.add("dropdownlist", undefined, ["Soft", "Medium", "Strong"]); glowStrengthList.selection = 0; var backgroundGroup = panel.add("group"); backgroundGroup.orientation = "row"; var backgroundCheckbox = backgroundGroup.add("checkbox", undefined, "Add background"); backgroundCheckbox.value = false; backgroundGroup.add("statictext", undefined, "Colour:"); var backgroundColourInput = backgroundGroup.add("edittext", undefined, "#03021F"); backgroundColourInput.characters = 9; var seedGroup = panel.add("group"); seedGroup.orientation = "row"; addLabel(seedGroup, "Variation number:", 120); var seedInput = seedGroup.add("edittext", undefined, ""); seedInput.characters = 8; seedGroup.add("statictext", undefined, "Optional: reuse a number to repeat the layout."); var note = dialog.add("statictext", undefined, "Leave Variation number blank for a new result each time."); note.alignment = "left"; var buttons = dialog.add("group"); buttons.alignment = "right"; buttons.add("button", undefined, "Cancel", {name: "cancel"}); buttons.add("button", undefined, "Generate", {name: "ok"}); if (dialog.show() !== 1) { return null; } var count = parseInt(countInput.text, 10); if (isNaN(count)) { count = 85; } count = clamp(count, 10, 250); var minimumStroke = parseFloat(minimumStrokeInput.text); var maximumStroke = parseFloat(maximumStrokeInput.text); if (isNaN(minimumStroke)) { minimumStroke = 0.55; } if (isNaN(maximumStroke)) { maximumStroke = 1.65; } minimumStroke = clamp(minimumStroke, 0.05, 50); maximumStroke = clamp(maximumStroke, 0.05, 50); if (maximumStroke < minimumStroke) { var swapStroke = minimumStroke; minimumStroke = maximumStroke; maximumStroke = swapStroke; } var minimumOpacity = parseFloat(minimumOpacityInput.text); var maximumOpacity = parseFloat(maximumOpacityInput.text); if (isNaN(minimumOpacity)) { minimumOpacity = 18; } if (isNaN(maximumOpacity)) { maximumOpacity = 92; } minimumOpacity = clamp(minimumOpacity, 1, 100); maximumOpacity = clamp(maximumOpacity, 1, 100); if (maximumOpacity < minimumOpacity) { var swapOpacity = minimumOpacity; minimumOpacity = maximumOpacity; maximumOpacity = swapOpacity; } var minimumLength = parseFloat(minimumLengthInput.text); var maximumLength = parseFloat(maximumLengthInput.text); if (isNaN(minimumLength)) { minimumLength = 40; } if (isNaN(maximumLength)) { maximumLength = 130; } minimumLength = clamp(minimumLength, 5, 500); maximumLength = clamp(maximumLength, 5, 500); if (maximumLength < minimumLength) { var swapLength = minimumLength; minimumLength = maximumLength; maximumLength = swapLength; } var palette = []; var colourEntries = colourInput.text.split(","); for (var paletteIndex = 0; paletteIndex < colourEntries.length; paletteIndex++) { var listedColour = colourFromHex(colourEntries[paletteIndex]); if (listedColour !== null) { palette.push(listedColour); } } if (palette.length === 0) { alert("Please enter at least one valid hex colour, such as #2674FF.", SCRIPT_NAME); return null; } var backgroundColour = colourFromHex(backgroundColourInput.text); if (backgroundColour === null) { if (backgroundCheckbox.value) { alert("Please enter a valid background hex colour, such as #03021F.", SCRIPT_NAME); return null; } backgroundColour = rgb(3, 2, 31); } var seedText = seedInput.text.replace(/\s/g, ""); var seed = null; if (seedText !== "") { seed = parseInt(seedText, 10); if (isNaN(seed)) { seed = null; } } return { count: count, rightHeavy: densityList.selection.index === 0, styleIndex: styleList.selection.index, palette: palette, minimumStroke: minimumStroke, maximumStroke: maximumStroke, minimumOpacity: minimumOpacity, maximumOpacity: maximumOpacity, minimumLength: minimumLength, maximumLength: maximumLength, glow: glowCheckbox.value, glowStrength: glowStrengthList.selection.index, background: backgroundCheckbox.value, backgroundColour: backgroundColour, seed: seed }; } function main() { var settings = showSettings(); if (settings === null) { return; } setRandomSeed(settings.seed); var document; if (app.documents.length === 0) { document = app.documents.add(DocumentColorSpace.RGB, 1920, 1080); } else { document = app.activeDocument; } var activeArtboard = document.artboards[document.artboards.getActiveArtboardIndex()]; var bounds = activeArtboard.artboardRect; var width = bounds[2] - bounds[0]; var height = bounds[1] - bounds[3]; var outputLayer = document.layers.add(); outputLayer.name = "Neon Scribbles " + (new Date().getTime()); var glowGroup = outputLayer.groupItems.add(); glowGroup.name = "Neon Glow"; var lineGroup = outputLayer.groupItems.add(); lineGroup.name = "Editable Lines"; var glowWidthMinimum = 3.2; var glowWidthMaximum = 5.2; var glowOpacityMinimum = 0.07; var glowOpacityMaximum = 0.15; var blurFactor = 2.2; if (settings.glowStrength === 1) { glowWidthMinimum = 4.2; glowWidthMaximum = 7.4; glowOpacityMinimum = 0.10; glowOpacityMaximum = 0.22; blurFactor = 3.0; } else if (settings.glowStrength === 2) { glowWidthMinimum = 6.0; glowWidthMaximum = 10.0; glowOpacityMinimum = 0.18; glowOpacityMaximum = 0.32; blurFactor = 4.5; } for (var lineIndex = 0; lineIndex < settings.count; lineIndex++) { var anchors = makeRandomAnchors( bounds, settings.rightHeavy, settings.minimumLength, settings.maximumLength, settings.styleIndex ); var colour = chooseColour(settings.palette); var coreWidth = randomBetween(settings.minimumStroke, settings.maximumStroke); var coreOpacity = randomBetween(settings.minimumOpacity, settings.maximumOpacity); var corePath = buildPath( lineGroup, anchors, colour, coreWidth, coreOpacity, "Line " + (lineIndex + 1), settings.styleIndex ); if (settings.glow) { // Duplicate the finished geometry so the glow follows it exactly. var glowPath = corePath.duplicate(glowGroup, ElementPlacement.PLACEATBEGINNING); glowPath.name = "Glow " + (lineIndex + 1); glowPath.strokeWidth = coreWidth * randomBetween(glowWidthMinimum, glowWidthMaximum); glowPath.opacity = coreOpacity * randomBetween(glowOpacityMinimum, glowOpacityMaximum); } } if (settings.glow) { try { var blurAmount = Math.max(1.5, settings.maximumStroke * blurFactor); var blurXML = ''; glowGroup.applyEffect(blurXML); } catch (effectError) { // The wider transparent under-strokes remain as a fallback glow. } } else { glowGroup.remove(); } if (settings.background) { var background = outputLayer.pathItems.rectangle(bounds[1], bounds[0], width, height); background.name = "Background"; background.stroked = false; background.filled = true; background.fillColor = copyColour(settings.backgroundColour); background.zOrder(ZOrderMethod.SENDTOBACK); background.locked = true; } app.redraw(); alert( settings.count + " editable neon lines created on the active artboard.\n\n" + "Run the script again whenever you want a different version.", SCRIPT_NAME ); } try { main(); } catch (error) { alert( "The generator stopped because Illustrator reported:\n\n" + error.message + (error.line ? "\n\nScript line: " + error.line : ""), SCRIPT_NAME ); } }());