AI Image Generator (free, no sign-up, unlimited) (2024)

⚄︎perchance

👥︎community

🌈hub

📚︎tutorial

📦︎resources

🎲︎generators

⊕︎new

🔑︎login/signup

✏️︎edit

`, "single-equals-in-dynamic-odds": `

Warning: The dynamic odds notation on line number (in generator's code) has a single equals sign instead of a double-equals sign. A single equals sign is used to put a value into a variable - for example age = 10 means "put the value 10 into the age variable" whereas age == 10 means "is age equal to 10?".

`, "single-equals-in-if-else-condition": `

Warning: The if/else condition on line number (in generator's code) has a single equals sign instead of a double-equals sign. A single equals sign is used to put a value into a variable - for example age = 10 means "put the value 10 into the age variable" whereas age == 10 means "is age equal to 10?".

`, "square-brackets-wrapping-variable-name-within-square-block": `

Warning: On line number (in generator's code), you've got a square block - i.e. some square brackets with some stuff inside. Nothing wrong with that. But inside that square block it looks like you've got another square block? Whenever you're writing stuff inside a square block, you can refer to variable and list name

directly

- no need to put them inside square blocks. So instead of [[noun].pluralForm], you'd just write [noun.pluralForm], and instead of [[characterAge] + 3], you'd just write [characterAge + 3], and instead of ^[[age] < 10], you'd just write ^[age < 10], and instead of [[age] > 60 ? [oldDescription] : [youngDescription]], you'd just write [age > 60 ? oldDescription : youngDescription]. You should use normal parentheses to group calculations and conditions, like this: [(age + 1) * height] and this: [isMammal || (isReptile && isBrown)]. In fact, if you wrap list/variable names in square brackets when you're already inside a square block then this can sometimes cause all sorts of sneaky errors that are hard to "debug". Square brackets

can

"legally" be used within square brackets in some circ*mstances, but they have a special meaning (learn more about this on the examples page - especially the "Dynamic Sub-list Referencing" section). One final note: It's okay to use square brackets within square brackets if the inner square brackets are themselves within double-quotes, like so: [n == 1 ? "[prefix]er" : "[prefix]ers"].

`, "top-level-list-name-same-as-html-element-id": `

Warning: It looks like you have a list name on line number that has the same name as an element's id="..." attribute? If so, note that these will conflict with one another and potentially cause errors. For example if you had a list called animal, then you shouldn't have an element like this: <p id="animal">...</p>

`, "duplicate-top-level-list-name": `

Warning: It looks like you have a list name on line number (in generator's code) that has the same name as an earlier list/import/variable?

`, }; this.generatorMetaData = undefined; this.toggleOutputEditorWrap = function() { let btn = this.refs.outputEditorRefButton; let state = this.outputTemplateEditor.getOption('lineWrapping'); if(state) { btn.innerText = "wrap"; } else { btn.innerText = "unwrap"; } this.outputTemplateEditor.setOption('lineWrapping', !state); }; this.toggleCodeEditorWrap = function() { let btn = this.refs.codeEditorWrapRefButton; let state = this.modelTextEditor.getOption('lineWrapping'); if(state) { btn.innerText = "wrap"; } else { btn.innerText = "unwrap"; } // record cursor position: let cursorLine = this.modelTextEditor.getCursor().line; let cursorTop = this.modelTextEditor.cursorCoords().top - this.modelTextEditor.display.wrapper.getBoundingClientRect().top; this.modelTextEditor.setOption('lineWrapping', !state); // recover scroll to cursor position: this.modelTextEditor.scrollIntoView({line:cursorLine, char:0}, cursorTop); }; this.allListsFolded = false; this.toggleCodeEditorFold = function() { let btn = this.refs.codeEditorFoldRefButton; let state = this.modelTextEditor.getOption('lineWrapping'); this.allListsFolded ? CodeMirror.commands.unfoldAll(this.modelTextEditor) : CodeMirror.commands.foldAll(this.modelTextEditor); this.allListsFolded = !this.allListsFolded; btn.innerText = this.allListsFolded ? "unfold" : "fold"; }; this.outputIframe = this.root.querySelector("#output iframe"); this.refs.autoReloadCheckbox.checked = this.autoReload; window.addEventListener("message", async (e) => { let origin = e.origin || e.originalEvent.origin; if(origin !== "https://null.perchance.org" && origin !== `https://${window.generatorPublicId}.perchance.org`) return; if(e.data.type === "evaluateTextResponse" && typeof e.data.text === "string" && typeof e.data.callerId === "string") { let resolveMap = this.evaluateTextResolveMap; if(resolveMap[e.data.callerId]) { resolveMap[e.data.callerId](e.data.text); delete resolveMap[e.data.callerId]; } } if(e.data.type === "requestOutputUpdate") { // for 'hard reload' without having to first save data to server // user clicks reload button which triggers a fullRefresh, but it adds __initWithDataFromParentWindow=1 to the URL, which causes the outputIframeContent.html code to not use the data embedded in the HTML, and instead request this parent frame for the 'fresh' data await window.mostRecentFullRefreshUpdateDependenciesPromise; // so dependency stuff can load in parrallel with iframe for __initWithDataFromParentWindow fullRefresh this.updateOutputRequest(); } if(e.data.type === "saveKeyboardShortcut") { app.handleIframeSaveRequest(); } // if(e.data.type === "finishedLoading") { // window.perchanceOutputIframeFinishedFirstLoad = true; // // initialPageLoadSpinner.style.display = "none"; // outputLoadSpinner.style.display = "none"; // } if(e.data.type === "finishedLoadingIncludingMetaData" || e.data.type === "failedToLoadDueToGeneratorErrors") { if(e.data.imports) { // <-- need to check because failedToLoadDueToGeneratorErrors doesn't give us imports // we need to check if there are new imports before resolving the hard reload stuff because the saveGenerator function uses a hard reload before saving (which triggers these messages and resolves below), and we want to make sure we save the generator with all its up-to-date dependencies included. let wereNewDeps = await window.lastImportsUpdateDependencyCheckPromise; if(wereNewDeps) return; // don't want to resolve yet - the importsUpdate handler will trigger a second hard reload } for(let resolver of window.outputIframeHardReloadFinishedResolvers) { resolver(); } window.outputIframeHardReloadFinishedResolvers = []; } if(e.data.type === "importsUpdate" && Array.isArray(e.data.imports) && e.data.imports.filter(i => typeof i !== 'string').length === 0) { await window.thisIsNotAnOldCachedPagePromise; // <-- wait until we've confirmed that this is the latest version of this generator. this prevents a weird race condition where an import update will happen fast during page load and beat the cache bust, which cause an auto-save (importsUpdates can do this), and overwrites the new code with old code. // (note that the above promise won't resolve unless it's true) window.lastImportsUpdateDependencyCheckPromise = this.updateDependencies(e.data.imports); let wereNewDeps = await window.lastImportsUpdateDependencyCheckPromise; // <-- this actually downloads new dependencies (**if needed**, else returns false) if(wereNewDeps) { // we only update if there were new dependencies otherwise we will get into infinite loops // (due to the way I've handled removing dependencies (I keep the cached still)) TODO: this whole // imports-update part is terrible and needs re-writing to make it simple and understandable. // Wouldn't take toooo much work. this.updateOutput(true); // <-- this triggers iframe updateOutput function. `true` => fullRefresh, i.e. fully reload the iframe with __initWithDataFromParentWindow app.generatorEditedCallback({imports:e.data.imports}); } else if(app.data.dependencies.length > e.data.imports.length) { // if dependency was *dropped*, no need to update the output - just need to update the data: app.generatorEditedCallback({imports:e.data.imports}); } } if(e.data.type === "metaUpdate") { if(e.data._validation.generatorName !== app.data.generator.name) return; // <-- trying to stop weird Google crawler page title bug // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ // WARNING WARNING WARNING WARNING WARNING WARNING WARNING: If you change this at all, make sure to do a thorough check for XSS bugs. Saved meta info must be sanitised ON THE SERVER during rendering. // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ let title = undefined; let description = undefined; let image = undefined; let dynamic = undefined; if(typeof e.data.html === "string") { if(e.data.html.length < 100_000 && window.shouldLiftGeneratorHtmlFromEmbed) { window.document.querySelector("#generator-description").innerHTML = DOMPurify.sanitize(e.data.html, {ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'p', 'div', 'pre', 'button', 'i', 'b', 'a', 'ul', 'li', 'br', 'ol', 'hr'], ALLOWED_ATTR: ['href']}); if(!window.generatorCanLinkWithRelFollow) window.document.querySelectorAll("#generator-description a").forEach(a => a.rel="nofollow"); } } if(typeof e.data.title === "string") { title = e.data.title.slice(0, 500); // MUST be sanitised on the server during rendering window.document.title = title + " ― Perchance" + (title.toLowerCase().includes("gener") ? "" : " Generator"); } if(typeof e.data.description === "string") { description = e.data.description.slice(0, 3000); // MUST be sanitised on the server during rendering } if(typeof e.data.image === "string") { image = e.data.image.slice(0, 500); // MUST be sanitised on the server during rendering } if(typeof e.data.dynamic === "string") { dynamic = e.data.dynamic.slice(0, 20000); } this.generatorMetaData = {title,image,description, dynamic}; } if(e.data.type === "codeWarningsUpdate") { let validWarningsCount = 0; this.refs.warningsModalOpenButton.style.display = "none"; this.refs.warningsWrapper.innerHTML = ""; let warningsHTML = ""; for(let warning of e.data.warnings) { if(typeof warning.lineNumber !== "number" || typeof warning.warningId !== "string" || /[^a-z0-9\-]/.test(warning.generatorName)) return; let warningTemplate = warnings[warning.warningId]; if(!warningTemplate) { console.error("invalid warning id?"); continue; } warningTemplate = warningTemplate.replace(/\{\{lineNumber\}\}/g, warning.lineNumber); let generatorNameText; if(warning.generatorName === window.location.pathname.slice(1)) generatorNameText = "this"; else generatorNameText = "the "+warning.generatorName+""; warningTemplate = warningTemplate.replace(/\{\{generatorNameText\}\}/g, generatorNameText); warningTemplate = warningTemplate.replace(/ \(in this<\/b> generator's code\)/g, ""); // hackily remove this, since it actually makes it more confusing for newbies I think. warningsHTML += warningTemplate; validWarningsCount++; if(validWarningsCount > 10) break; } if(validWarningsCount > 0) { this.refs.warningsModalOpenButton.style.display = "inline-block"; this.refs.warningsWrapper.innerHTML = warningsHTML; this.refs.warningsModal.querySelector(".modal-body").scrollTop = 0; } } // if(e.data.type === "requestFullscreen") { // if(this.outputIframe.requestFullscreen) this.outputIframe.requestFullscreen(); // else if(this.outputIframe.webkitRequestFullscreen) this.outputIframe.webkitRequestFullscreen(); // else if(this.outputIframe.mozRequestFullscreen) this.outputIframe.mozRequestFullscreen(); // else if(this.outputIframe.msRequestFullscreen) this.outputIframe.msRequestFullscreen(); // } // // if(e.data.type === "exitFullscreen") { // if(this.outputIframe.exitFullscreen) this.outputIframe.exitFullscreen(); // else if(this.outputIframe.webkitExitFullscreen) this.outputIframe.webkitExitFullscreen(); // else if(this.outputIframe.mozCancelFullscreen) this.outputIframe.mozCancelFullscreen(); // else if(this.outputIframe.msExitFullscreen) this.outputIframe.msExitFullscreen(); // } }); // codemirror settings CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; CodeMirror.keyMap.default["Tab"] = "indentMore"; this.root.querySelector("#input").value = this.root.querySelector("#input").value.replace(/\n[\t ]+/g, (match) => match.replace(/\\t/g, " ")); // I'm reluctant to mess with tabs in the HTML editor because they are significant in e.g.

, and I'm not sure if replacing with entity () would actually work in all cases. Should be solved for all future generators anyway (via pasting intercept code below). /*this.modelTextEditor = CodeMirror.fromTextArea(this.root.querySelector("#input"), { lineNumbers: true, foldGutter: true, extraKeys: { //"Ctrl-Q": cm => cm.foldCode(cm.getCursor()), //"Ctrl-Y": cm => CodeMirror.commands.foldAll(cm), //"Ctrl-I": cm => CodeMirror.commands.unfoldAll(cm), }, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], tabSize: 2, indentUnit: 2, indentWithTabs: false, matchBrackets: true, mode: localStorage.codeMirrorMode || "simplemode", styleActiveLine: true, //mode: "text", lineWrapping:false, //theme: "monokai", keyMap: "sublime", });*/ let systemIsInDarkMode = !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); this.modelTextEditor = CodeMirror.fromTextArea(this.root.querySelector("#input"), { lineNumbers: true, foldGutter: true, extraKeys: { //"Ctrl-Q": cm => cm.foldCode(cm.getCursor()), //"Ctrl-Y": cm => CodeMirror.commands.foldAll(cm), //"Ctrl-I": cm => CodeMirror.commands.unfoldAll(cm), }, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], tabSize: 2, indentUnit: 2, indentWithTabs: false, matchBrackets: true, mode: "perchancelists", styleActiveLine: true, lineWrapping:false, theme: systemIsInDarkMode ? "one-dark" : "one-light", keyMap: "sublime", highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}, }); // hacky fix for a bug where if you type two consecutive backticks, the `perchancelists` mode's JS state gets messed up for some reason. // we just retokenize when backtick is typed. let backtickHighlightRetokenizeTimeout = null; this.modelTextEditor.on("beforeChange", function(cm, changeObj) { if(changeObj.text.some(line => line.includes('`'))) { clearTimeout(backtickHighlightRetokenizeTimeout); backtickHighlightRetokenizeTimeout = setTimeout(() => cm.setOption("mode", "perchancelists"), 200); } }); this.outputTemplateEditor = CodeMirror.fromTextArea(this.root.querySelector("#template"), { // lineNumbers: true, // foldGutter: true, // gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], mode: {name: "htmlmixed"}, selectionPointer: true, tabSize: 2, indentUnit: 2, indentWithTabs: false, lineWrapping:false, styleActiveLine: true, theme: systemIsInDarkMode ? "one-dark" : "one-light", keyMap: "sublime", highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: true}, }); window.editsHaveBeenMadeSincePageLoad = false; // these are also called after the generator is saved, so long as window.lastEditorsChangeTime is the same as the instant that the save operation was initiated. // codemirror will make editor.isClean() return true when it comes back to this state, **including via undo** this.modelTextEditor.markClean(); this.outputTemplateEditor.markClean(); // so the save code can access them for markClean stuff: window.modelTextEditor = this.modelTextEditor; window.outputTemplateEditor = this.outputTemplateEditor; // this is also set in the save handler. they're used as an extra check in doLocalGeneratorBackupIfNeeded to prevent backing up text that is already saved. window.lastModelTextSaved = this.modelTextEditor.getValue(); window.lastOutputTemplateSaved = this.outputTemplateEditor.getValue(); // this is stupid but it's the only way I could get chrome to stop suggesting an email/password in codemirror's search dialogue try { function watchForChildWithClass(target, className, callback) { new MutationObserver(mutations => { mutations.forEach(mutation => { Array.from(mutation.addedNodes).forEach(node => { if(node.nodeType === 1 && node.classList.contains(className)) callback(node); }); }); }).observe(target, { childList: true }); } setTimeout(() => { for(let container of Array.from(document.querySelectorAll(".CodeMirror"))) { watchForChildWithClass(container, "CodeMirror-dialog", element => { for(let inputEl of Array.from(element.querySelectorAll("input"))) { let dummyEl = document.createElement("div"); dummyEl.innerHTML = ``; dummyEl.style.cssText = "opacity:0; pointer-events:none; position:absolute;"; inputEl.parentElement.insertBefore(dummyEl, inputEl); } }); } }, 400); } catch(e) { console.error(e); } // there was some funky business happening with tabs when people *pasted* them. // This replaces all tabs with two spaces (users need to write \t if they want a tab in their text). // EDIT: also just added non-breaking space replacement with normal space, since it was messing with indenting stuff. this.modelTextEditor.on("beforeChange", (cm, change) => { if(change.origin === "paste") { change.update(null, null, change.text.map(line => line.replace(/\t/g, " ").replaceAll(`\u00A0`, " "))); } }); // I took this out because in HTML, some text with to consecutive spaces is different to , and I think the same is true of tabs. So I think this would turn code indents into a bunch of visible whitespace. // this.outputTemplateEditor.on("beforeChange", (cm, change) => { // if(change.origin === "paste") { // change.update(null, null, change.text.map(line => line.replace(/\t/g, ""))); // } // }); // This is hacky, but it seems to work. It's from here: https://codemirror.net/demo/indentwrap.html // It makes it so wrapped text is indented at the same indent level as the line itself. let basePadding = 4; // Only do the indentwrap stuff if there are no tabs (literal `\t` is fine), because otherwise it (for some reason) doesn't work - it messes things up: jsbin.com/tukuximome if(!this.root.querySelector("#template").value.includes("\t")) { let charWidthOutputTemplate = this.outputTemplateEditor.defaultCharWidth(); this.outputTemplateEditor.on("renderLine", function(cm, line, elt) { let off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidthOutputTemplate; elt.style.textIndent = "-" + off + "px"; elt.style.paddingLeft = (basePadding + off) + "px"; }); } if(!this.root.querySelector("#input").value.includes("\t")) { let charWidthModelText = this.modelTextEditor.defaultCharWidth(); this.modelTextEditor.on("renderLine", function(cm, line, elt) { let off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidthModelText; elt.style.textIndent = "-" + off + "px"; elt.style.paddingLeft = (basePadding + off) + "px"; }); } ////////////////////////////////////// // LOCAL STORAGE BACKUPS // ////////////////////////////////////// const maxLocalBackupInterval = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*5 : 1000*60*5; // CAUTION: if you change this, change `doLocalBackupDebounce6MinTimeout` to be larger than it const oldLocalBackupCleaningInterval = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*60*2 : 1000*60*20; const maxLocalBackupAge = localStorage.__speedUpLocalBackupStuffForTesting ? 1000*60*10 : 1000*60*60*24*30; const maxLocalBackupsPerGenerator = localStorage.__speedUpLocalBackupStuffForTesting ? 10 : 30; if(localStorage.__speedUpLocalBackupStuffForTesting) { console.warn(`WARNING: local backups sped up due to truthy localStorage.__speedUpLocalBackupStuffForTesting value\n`.repeat(20)); } let lastLocalBackupTime = Date.now(); const doLocalGeneratorBackupIfNeeded = async (modelText, outputTemplate) => { if(!app.userOwnsThisGenerator) return; // they don't own this gen (i.e. are just playing around in editor for now) if(Date.now()-window.generatorLastSaveTime < maxLocalBackupInterval) return; // hasn't been a few mins since last save if(Date.now()-lastLocalBackupTime < maxLocalBackupInterval) return; // at most once every few mins if(window.lastModelTextSaved === modelText && window.lastOutputTemplateSaved === outputTemplate) return; // since they may have made an edit, but then undone it lastLocalBackupTime = Date.now(); if(!kv) await window.initKv(); let backupsArray = await kv.localBackups.get(app.data.generator.name) || []; backupsArray.unshift({modelText, outputTemplate, time:Date.now()}); // if there are more than `maxLocalBackupsPerGenerator` backups, delete every second one in the second half of the array: if(backupsArray.length > maxLocalBackupsPerGenerator) { backupsArray = backupsArray.filter((backup, i) => i < maxLocalBackupsPerGenerator/2 ? true : i%2===0); } await kv.localBackups.set(app.data.generator.name, backupsArray); console.log("Saved local backup. NEW backupsArray:", backupsArray); }; setTimeout(async () => { if(app.userOwnsThisGenerator) { let persistent = await navigator.storage.persist(); if(!persistent) console.warn("Browser denied persistent local storage."); } }, 1000*60*20); setInterval(async () => { if(!kv) return; let entries = await kv.localBackups.entries(); // for *every* backed-up generator (not just the one they're currently viewing/editing) stored in this user's browser: for(let [generatorName, backupsArray] of entries) { let originalLength = backupsArray.length; // delete the backup array entries older than several weeks: backupsArray = backupsArray.filter(backup => Date.now()-backup.time < maxLocalBackupAge); if(originalLength === backupsArray.length) continue; // save changes: if(backupsArray.length === 0) { await kv.localBackups.delete(generatorName); await kv.localBackupsLastWarnTimes.delete(generatorName); } else { console.log(`Cleared ${originalLength-backupsArray.length} old backups. NEW backupsArray:`, backupsArray); await kv.localBackups.set(generatorName, backupsArray); } } }, oldLocalBackupCleaningInterval); // if the page loads, and user owns this generator, and the last backup time is more than a few mins forward in time from last save, then warn them to click header 'backups' button setTimeout(async () => { let startTime = Date.now(); while(app.userOwnsThisGenerator === undefined) await new Promise(r => setTimeout(r, 1000*10)); if(Date.now()-startTime > 1000*60) { console.warn(`Took a ${Date.now()-startTime}ms (i.e. much longer than expected) to determine if this user owns this generator?`); return; } if(!app.userOwnsThisGenerator) return; if(!kv) await window.initKv(); let localBackupsArr = await kv.localBackups.get(app.data.generator.name); let lastWarnTimeForThisGen = await kv.localBackupsLastWarnTimes.get(app.data.generator.name) || 0; if(localBackupsArr) { for(let data of localBackupsArr) { if(data.time-window.generatorLastSaveTime > 1000*60*3 && data.time > lastWarnTimeForThisGen) { try { app.goToEditMode(); document.querySelector(`#menuBarEl [data-ref="revisionsButton"]`).style.backgroundColor = "#7d5100"; } catch(e) { console.error(e); } await kv.localBackupsLastWarnTimes.set(app.data.generator.name, Date.now()); alert(`Warning: There's a backup of this generator stored in your browser memory which is newer than the version of this generator you're currently viewing. This could have been caused by a problem where the server didn't properly save your generator, or if your browser previously crashed while you were editing, or it could just be that you made a little edit but then decided not to save it. If you lost some work, click the 'backups' button in the header to download the code for a backed-up version.`); return; } } } }, 1000*3); let editorContentAlreadySavedDebounceTimeout; function checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate) { clearTimeout(editorContentAlreadySavedDebounceTimeout); editorContentAlreadySavedDebounceTimeout = setTimeout(() => { if(window.lastModelTextSaved === modelText && window.lastOutputTemplateSaved === outputTemplate) { if(app.userOwnsThisGenerator) { window.setSaveState("saved"); } } }, 300); } let doLocalBackupDebounce6MinTimeout; window.lastEditorsChangeTime = Date.now(); this.outputTemplateEditor.on("changes", () => { window.lastEditorsChangeTime = Date.now(); window.editsHaveBeenMadeSincePageLoad = true; let modelText = this.modelTextEditor.getValue(); let outputTemplate = this.outputTemplateEditor.getValue(); app.generatorEditedCallback({modelText, outputTemplate}); if(this.outputTemplateEditor.isClean() && this.modelTextEditor.isClean()) { // in case they undo and go back to a saved state if(app.userOwnsThisGenerator) { window.setSaveState("saved"); // must come after generatorEditedCallback, above, since that sets the save state to "unsaved" } } else { checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate); } try { doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); clearTimeout(doLocalBackupDebounce6MinTimeout); doLocalBackupDebounce6MinTimeout = setTimeout(() => { // without this, we can still lose up to 5 minutes of work, which is annoying doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); }, 1000*60*6); } catch(e) { console.error(e); } }); this.modelTextEditor.on("changes", () => { window.lastEditorsChangeTime = Date.now(); window.editsHaveBeenMadeSincePageLoad = true; let modelText = this.modelTextEditor.getValue(); let outputTemplate = this.outputTemplateEditor.getValue(); app.generatorEditedCallback({modelText, outputTemplate}); if(this.outputTemplateEditor.isClean() && this.modelTextEditor.isClean()) { // in case they undo and go back to a saved state if(app.userOwnsThisGenerator) { window.setSaveState("saved"); // must come after generatorEditedCallback, above, since that sets the save state to "unsaved" } } else { checkIfEditorContentAlreadySavedWithDebounce(modelText, outputTemplate); } try { doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); clearTimeout(doLocalBackupDebounce6MinTimeout); doLocalBackupDebounce6MinTimeout = setTimeout(() => { // without this, we can still lose up to 5 minutes of work, which is annoying doLocalGeneratorBackupIfNeeded(modelText, outputTemplate); }, 1000*60*6); } catch(e) { console.error(e); } }); // disable overwrite insert mode: this.outputTemplateEditor.on("keyup", () => { this.outputTemplateEditor.toggleOverwrite(false); }); this.modelTextEditor.on("keyup", () => { this.modelTextEditor.toggleOverwrite(false); }); this.outputTemplateEditor.on("changes", () => { clearTimeout(this.updateOutputDelayTimeout); if(this.autoReload) { this.updateOutputDelayTimeout = setTimeout(() => { this.updateOutput(false, true); }, 800); } }); this.modelTextEditor.on("changes", () => { clearTimeout(this.updateOutputDelayTimeout); if(this.autoReload) { this.updateOutputDelayTimeout = setTimeout(() => { this.updateOutput(false, true); // EDIT: this is commented out because it doesn't make sense - if they haven't reloaded, then the computed meta data is obviously going to be using the old code. // TODO: Make it so that when they send the save request, that's when we send updateMetadataIfNeeded, and we send the updated modelText which is executed with createPerchanceTree or whatever, but is then discarded, rather than replacing the "real" tree and updating everything. // this.outputIframe.contentWindow.postMessage({command:"updateMetadataIfNeeded"}, '*'); // needed because they may not have 'auto' checked, and so they make some changes to their $meta props, then hit save, but the updated $meta stuff isn't sent along with the save request. }, 800); } }); // if(window.DEBUG_FREEZE_MODE) { // this.refs.autoReloadCheckbox.checked = false; // //this.root.querySelector("#output iframe").src = "https://null.perchance.org/debug-freeze"; // } else { // // let url = "https://null.perchance.org/" + app.data.generator.name + "?v=1"; // // let url = `https://${window.generatorPublicId}.perchance.org/` + app.data.generator.name + "?v=2"; // // if(window.needToBustCacheOfIframeOnInitialLoad) url += `?__cacheBuster=${Math.random()}`; // let url = new URL(window.location.href); // url.hostname = `${window.generatorPublicId}.perchance.org`; // if(window.needToBustCacheOfIframeOnInitialLoad) { // url.searchParams.set("__cacheBust", Math.random().toString()+Math.random().toString()); // } // // This is to ensure cache is busted even if they have URL params added to the URL. // // Cloudflare has limits to the number of cache clears, so this is a hacky but full-proof workaround. // // The top-level frame has the `window.thisIsNotAnOldCachedPagePromise` stuff to handle this, but we still // // need to make sure the iframe is also busted. // // We only need this during initial page load - not when reload button / save button is pressed, since in those // // cases the __initWithDataFromParentWindow query param is added to the URL, which means the iframe gets fresh // // data from the parent, regardless of the html that the server sends (i.e. the data that's embedded in the HTML // // is ignored) // url.searchParams.set("__generatorLastEditTime", window.generatorLastSaveTime); // this.root.querySelector("#output iframe").src = url; // } if(window.DEBUG_FREEZE_MODE) { this.refs.autoReloadCheckbox.checked = false; this.root.querySelector("#output iframe").src = "https://null.perchance.org/debug-freeze"; } else if(window.needToBustCacheOfIframeOnInitialLoad) { let url = new URL(window.location.href); url.hostname = `${window.generatorPublicId}.perchance.org`; url.searchParams.set("__cacheBust", Math.random().toString()+Math.random().toString()); this.root.querySelector("#output iframe").src = url; } // whenever the codemirror scrollbar appears or dissapears, update the buttons positioning so they sit beside the scrollbar: const updateCodeEditorButtonsPos = () => { document.querySelector(".code-editor-buttons-ctn").style.right = (document.querySelector(".CodeMirror-vscrollbar").offsetWidth+5)+"px"; }; new MutationObserver(updateCodeEditorButtonsPos).observe(document.querySelector(".CodeMirror-vscrollbar"), {attributes: true}); setTimeout(updateCodeEditorButtonsPos, 2000); // <-- call it once at the start for init // same for html area: const updateHtmlEditorButtonsPos = () => { document.querySelector(".toggle-wrap-button-html").style.right = (document.querySelectorAll(".CodeMirror-vscrollbar")[1].offsetWidth+5)+"px"; }; new MutationObserver(updateHtmlEditorButtonsPos).observe(document.querySelectorAll(".CodeMirror-vscrollbar")[1], {attributes: true}); setTimeout(updateHtmlEditorButtonsPos, 2000); // <-- call it once at the start for init // Initialise Split.js panels let sizesStore = new Store('editor-split-sizes'); let sizes; let defaultSizes = { ab: [60,40], cd: [90,10], ef: [75,25], }; if(sizesStore.data[app.data.generator.name]) { sizes = sizesStore.data[app.data.generator.name]; } else { // default sizes sizes = JSON.parse(JSON.stringify(defaultSizes)); sizesStore.data[app.data.generator.name] = sizes; sizesStore.save(); } function trimSplitSizes(arr) { // for some reason split sizes sometimes add to over 100% and the editor breaks. // this trims it to 100% arr = arr.slice(0); let sum = arr[0] + arr[1]; if(sum <= 100) return arr; else { arr[0] = Math.round(arr[0]); arr[1] = Math.round(arr[1]); while(arr[0] + arr[1] > 100) { if(Math.random() < 0.5) { arr[0]--; } else { arr[1]--; } } return arr; } } let split_ab = Split([this.root.querySelector('#a'), this.root.querySelector('#b')], { gutterSize: 8, sizes: sizes.ab, cursor: 'col-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.ab = split_ab.getSizes(); data.ab = trimSplitSizes(data.ab); sizesStore.save(); } }); let split_cd = Split([this.root.querySelector('#c'), this.root.querySelector('#perchanceConsoleEl')], { direction: 'vertical', sizes: sizes.cd, gutterSize: 8, cursor: 'row-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.cd = split_cd.getSizes(); data.cd = trimSplitSizes(data.cd); sizesStore.save(); } }); let split_ef = Split([this.root.querySelector('#e'), this.root.querySelector('#f')], { direction: 'vertical', sizes: sizes.ef, gutterSize: 8, cursor: 'row-resize', minSize: 30, onDragEnd: function() { let data = sizesStore.data[app.data.generator.name]; if(!data) { data = JSON.parse(JSON.stringify(defaultSizes)); } // they could have changed generator's name data.ef = split_ef.getSizes(); data.ef = trimSplitSizes(data.ef); sizesStore.save(); } }); // need this because otherwise for some reason there's some weird `overscroll-behaviour` stuff near the edges of the screen (even with touch-action:none overlay workaround). if(window.location.hash !== "#edit") this.root.querySelectorAll("#main .gutter").forEach(el => el.style.display="none"); setTimeout(() => { this.modelTextEditor.refresh(); this.outputTemplateEditor.refresh(); }, 10); ////////////////////// // CONSOLE // ////////////////////// function escapeHtml(html) { return html.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); } let consoleOutputStyle = ""; let consoleCommandStore = new Store("console-commands"); if(!consoleCommandStore.data.generators) { consoleCommandStore.data.generators = {}; } let consoleOutputEl = document.querySelector("#console-output"); let consoleInputEl = document.querySelector("#console-input"); consoleOutputEl.addEventListener("click", function() { consoleInputEl.focus(); }); consoleOutputEl.srcdoc = consoleOutputStyle + `e.g. try typing I rolled a \{1-6\}! or [yourListName] (including the brackets) above, and then press enter.`; let previousCommands = consoleCommandStore.data.generators[app.data.generator.name] || [""]; let currCommandIndex = 0; // NOT previousCommands array index!! 0 represents most recent element, 1 is second most recent, etc. consoleInputEl.addEventListener("keydown", async (e) => { // e.preventDefault(); let command = consoleInputEl.value; if(e.which === 13) { // enter let optEl = document.querySelector("#consoleInterpreterOption"); let interpreter = optEl.options[optEl.selectedIndex].value; if(interpreter === "plaintext") { consoleOutputEl.srcdoc = consoleOutputStyle.replace("font-family:monospace;", "font-family:monospace; white-space:pre-wrap;")+(escapeHtml(await this.evaluateText(command))); } else if(interpreter === "html") { consoleOutputEl.srcdoc = consoleOutputStyle+(await this.evaluateText(command)); } // this.root.style.background = "#f1f1f1"; // setTimeout(() => { this.root.style.background = "white"; },100) let prevCommand = previousCommands[previousCommands.length-1]; if(prevCommand !== command) { previousCommands.pop(); previousCommands.push(command); previousCommands.push(""); // put the empty one back on the end } if(previousCommands.length > 1000) { previousCommands.shift(); } currCommandIndex = 1; // <-- set index to that of the command that was just submitted consoleCommandStore.data.generators[app.data.generator.name] = previousCommands; // (not necessary because it keeps reference??) consoleCommandStore.save(); return; } if(e.which === 38) { // up arrow currCommandIndex++; // move towards start of array (counter-intuitive) currCommandIndex = Math.min(previousCommands.length-1, currCommandIndex); // must stay below or at length of command stack consoleInputEl.value = previousCommands[previousCommands.length-1-currCommandIndex]; setTimeout(function() { consoleInputEl.selectionStart = consoleInputEl.selectionEnd = 100000; }, 1); return; } if(e.which === 40) { // down arrow currCommandIndex--; // move towards end off array (counter-intuitive) currCommandIndex = Math.max(0, currCommandIndex); // must stay above or at zero consoleInputEl.value = previousCommands[previousCommands.length-1-currCommandIndex]; setTimeout(function() { consoleInputEl.selectionStart = consoleInputEl.selectionEnd = 100000; }, 1); return; } }); }, }; 

AI Image Generator (free, no sign-up, unlimited) (2024)

FAQs

Is there a free AI art generator with no restrictions? ›

Gcore AI Image Generator is free and has no usage restrictions. To start creating, log in to the Gcore Cloud platform. If you are not registered, sign up using your email, Google, or GitHub account.

How to create unlimited AI images for free? ›

Can I generate AI images for free? Absolutely! You can generate unlimited images for free using Microsoft Designer—all you need is a Microsoft account, which is also free to set up. If you don't have one yet, select Create account when asked to sign in and you'll be ready to go in under two minutes.

Is there a free NSFW AI generator without restrictions? ›

Top NSFW AI Generators With No Restrictions
  • DALL-E. DALL-E is one of the most well-known and advanced AI image generators on the market today. ...
  • Stable Diffusion. ...
  • Disco Diffusion. ...
  • Midjourney.

Which AI image editor has no limits? ›

The Neurapix Flat rate is particularly suitable for photographers who regularly have to shoot and deliver large quantities of images. You simply edit as much as you want – without any limits!

Which AI image generator has no adult restrictions? ›

If you want to generate uncensored AI images, you should go to the Perchance AI website and select the AI image generator image feature. We tested Perchance AI and found that it did not flag mature and adult words in the prompt– making it a truly uncensored image generator.

Is there a free AI I can use? ›

Copy.ai. Copy.ai is an exceptional free AI tool that has revolutionized the way content is generated, making it an indispensable asset for marketers, content creators, and businesses of all sizes. This AI-powered platform uses advanced natural language processing algorithms to generate high-quality content quickly.

Is there an NSFW filter on character AI? ›

Character AI is a widely popular chatbot that allows discussions on a vast array of topics. It has gained significant recognition on TikTok, Instagram, and various other online platforms. However, one aspect that has drawn criticism from some users is the presence of the NSFW AI Chat filter.

What is the AI that allows inappropriate content? ›

Crushon AI

Crushon AI allows users to engage in NSFW Character AI chats without filters. It offers an immersive experience where conversations feel genuine and unrestricted, akin to interacting with a virtual girlfriend.

What is the AI with no NSFW restrictions? ›

While traditional AI platforms may have limitations in addressing NSFW topics, a revolutionary solution has emerged - CrushOn.AI. The only platform that can truly address the need for an alternative NSFW AI is CrushOn.AI.

What is the most powerful free AI image generator? ›

Best AI Image Generators of 2024
  • See at OpenAI. Dall-E 3. Best overall AI image generator. See at OpenAI.
  • See at Leonardo AI. Leonardo AI. Best free AI image generator. See at Leonardo AI.
  • See at Adobe. Adobe Firefly. Best AI image generator for professionals. ...
  • See at Canva. Canva. Best basic AI image generator for beginners.
Jun 28, 2024

Is there an AI that has no limits? ›

Introducing UnrestrictedAI, the no-holds-barred AI chatbot that's ready to take on any challenge. Whether you need code assistance, want to engage in provocative conversations, or seek help with cheating, UnrestrictedAI has got you covered.

What is the app everyone is using for AI images? ›

If you've logged on to any social media app this week, you've probably seen pictures of your friends, but re-imagined as fairy princesses, animé characters, or celestial beings. It is all because of Lensa, an app which uses artificial intelligence to render digital portraits based on photos users submit.

Is there any AI without restrictions? ›

Introducing UnrestrictedAI, the no-holds-barred AI chatbot that's ready to take on any challenge. Whether you need code assistance, want to engage in provocative conversations, or seek help with cheating, UnrestrictedAI has got you covered.

Can you freely use AI art? ›

Yes, you can generally use AI-generated art legally, But there are a few things to keep in mind. AI-generated art is protected by copyright if it is original and creative. This means that you cannot copy or modify existing copyrighted works without permission from the copyright holder.

Is there any free AI content generator? ›

1. Jasper AI – Best for Free Image Generation and AI Copywriting. Jasper is one of the most impressive AI content generators on the market. It offers a range of features, including pre-set templates for a variety of writing formats, built-in SEO checking, plagiarism detection, brand voices, and even image generation.

References

Top Articles
Latest Posts
Article information

Author: Lidia Grady

Last Updated:

Views: 5930

Rating: 4.4 / 5 (45 voted)

Reviews: 84% of readers found this page helpful

Author information

Name: Lidia Grady

Birthday: 1992-01-22

Address: Suite 493 356 Dale Fall, New Wanda, RI 52485

Phone: +29914464387516

Job: Customer Engineer

Hobby: Cryptography, Writing, Dowsing, Stand-up comedy, Calligraphy, Web surfing, Ghost hunting

Introduction: My name is Lidia Grady, I am a thankful, fine, glamorous, lucky, lively, pleasant, shiny person who loves writing and wants to share my knowledge and understanding with you.