1<!DOCTYPE html> 2<html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width"> 6 <link rel="canonical" href="https://emojicombos.com/emoji-art-editor"> 7 <title>Make Emoji Art - Online Emoji Art Maker Tool</title> 8
8<script src="https://cdn.jsdelivr.net/npm/[email protected]/index.min.js"></script>
8 9
9<script>window.graphemeSplitter=new GraphemeSplitter</script>
9 10 </head> 11 <body> 12 <link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet"> 13 14 <style> 15 body { 16 background: #eee; 17 font-family: 'Roboto', sans-serif; 18 } 19 body * { 20 box-sizing: border-box; 21 } 22 .step-module { 23 width: 95vw; 24 max-width: 900px; 25 margin: 0 auto; 26 margin-bottom: 1rem; 27 padding: 0.25rem; 28 border-radius: 3px; 29 box-shadow: 0px 1px 1px #b4b4b4; 30 background: white; 31 } 32 .step-module .step-header { 33 border-bottom: 1px solid lightgrey; 34 margin:0.5rem; 35 margin-top:0rem; 36 } 37 .step-module .step-body { 38 margin:0.5rem; 39 } 40 .step-body .step-body-section { 41 display:flex; 42 margin: 0.5rem 0; 43 align-items: center; 44 } 45 .b { 46 font-size: inherit; 47 line-height: inherit; 48 } 49 .tool-button[data-active='1'] { 50 font-weight: bold; 51 } 52 53 button { 54 cursor: pointer; 55 } 56 57 .tool-button { 58 margin: 0.25rem; 59 padding: 0.25rem; 60 } 61 </style> 62 63 <h1 style="text-align:center; font-size:1.5rem;">Make Emoji Art</h1> 64 <h2 style="overflow:hidden; height:0px; margin:0;">Edit and <b>draw emoji art</b> using this simple <b>emoji art editor</b>.</h2> 65 66 <p style="text-align:center; font-size:0.8rem; color:grey; max-width:400px; margin:0 auto;margin-bottom: 1rem;">Also try our <a style="" href="/dot-art-editor" target="_blank">dot art editor</a> and our <a style="" href="/dot-art-generator" target="_blank">image to dot art generator</a>.</p> 67 68 <!--<div class="step-module"> 69 <h3 class="step-header">Choose grid size:</h3> 70 <div class="step-body"> 71 <div class="step-body-section"> 72 <input id="titleEl" style="width:100%; height:min-content;" placeholder="My Cool Thing" oninput="urlPreviewEl.innerHTML=`emojicombos.com/${normalizeKeyphrase(this.value).replace(/-/g, '--').replace(/ /g, '-')}`;"> 73 </div> 74 </div> 75 </div>--> 76 77 <div style="text-align:center; margin-bottom:0.5rem;"> 78 <button onclick="openEmojiPicker()" class="tool-button">Emoji: <span id="currentEmojiDisplayEl"></span></button> 79 <button onclick="undo()" class="tool-button">â¶ undo</button> 80 <button onclick="redo()" class="tool-button">â· redo</button> 81 <button onclick="clearButtonClickHandler()" class="tool-button">clear/new</button> 82 <button onclick="cropButtonClickHandler()" class="tool-button">crop to fit</button> 83 <button onclick="addPaddingClickHandler()" class="tool-button">add padding</button> 84 <button onclick="importButtonClickHandler()" class="tool-button">import</button> 85 <button onclick="zoomOut()" class="tool-button">ð-</button> <!-- gridEl.style.lineHeight=(Number(gridEl.style.lineHeight.slice(0,-2))/1.5)+'px'; --> 86 <button onclick="zoomIn()" class="tool-button">ð+</button> 87 </div> 88 89 <div id="currentPaletteListEl" style="text-align: center; margin-bottom: 0.5rem;"></div> 90 91 <div id="gridEl" style="border:1px solid grey; white-space:pre; font-size:15px; margin:0 auto; width:min-content; user-select:none; overscroll-behavior-y:none; cursor:default;"></div> 92 93 <div style="text-align:center; margin-top:0.5rem; font-size:1.5rem;"> 94 <button style="font-size:inherit;" onclick='copyEmojiArtClickHandler(gridEl.textContent),this.innerHTML="â copied!",this.disabled=!0,setTimeout(()=>{this.innerHTML="ð copy",this.disabled=!1},1500)'>ð copy</button> 95 <button style="font-size:inherit;" onclick="openSaveTextWindow(gridEl.textContent)">ð¾ save/submit</button> 96 </div> 97 98 <div id="emojiSearchModal" style="margin-top: 0.25rem; display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgb(0,0,0,0.5);"> 99 <div id="emojiSearchModalInner" style="margin: auto; width: 90vw; max-width: 700px; background: white; padding: 0.25rem; border-radius: 2px;"> 100 <div class="search-header"><input id="emojiSearchInputEl" style="flex-grow:1; font-size:1.3rem;" placeholder="Search for emojis..."></div> 101 <div id="emojiSearchResultsCtn"></div> 102 </div> 103 </div> 104 <style> 105 #emojiSearchModal .search-header { 106 margin: 0.25rem; 107 background: #e3e3e3; 108 width: auto; 109 padding: 0.25rem; 110 border-radius: 2px; 111 display: flex; 112 align-items: center; 113 } 114 #emojiSearchResultsCtn { 115 display: flex; 116 flex-wrap: wrap; 117 max-height: 30vh; 118 overflow-y: auto; 119 justify-content: space-between; 120 } 121 #emojiSearchResultsCtn > div { 122 padding: 0.4rem; 123 margin:0.25rem; 124 border: 1px solid #c4c4c4; 125 border-radius: 2px; 126 cursor:pointer; 127 font-size: 130%; 128 user-select: none; 129 } 130 #emojiSearchResultsCtn > div:hover { 131 background: #f6f6f6; 132 } 133 </style> 134
134<script>let emojiSearchDebounceTimeout;function emojiSearchResultClickHandler(e){updateCurrentEmoji(this.textContent),updateEmojiPalette(),closeEmojiPicker()}emojiSearchInputEl.addEventListener("input",function(){emojiSearchResultsCtn.innerHTML='<svg style="margin:1rem auto;" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"><animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite"/></path></svg>',clearTimeout(emojiSearchDebounceTimeout),emojiSearchCache[emojiSearchInputEl.value]?updateEmojiSearchResults(emojiSearchInputEl.value):emojiSearchDebounceTimeout=setTimeout(()=>{updateEmojiSearchResults(emojiSearchInputEl.value)},400)});let emojiSearchCache={};async function updateEmojiSearchResults(e){let t;emojiSearchCache[e]?t=emojiSearchCache[e]:(t=await fetch(`https://emojicombos.com/api/keywordsToEmojis?keywords=${encodeURIComponent(e)}&key=${localStorage.loginKey||""}`).then(e=>e.json()).catch(e=>(console.error(e),[])),emojiSearchCache[e]=t);t=t.map(e=>e.trim()).filter(e=>/(\p{Emoji}\uFE0F|\p{Emoji_Presentation})/u.test(e)&&1===graphemeSplitter.splitGraphemes(e).length);let a=t.filter(e=>e.trim()).map(e=>`<div>${e.replace(/</g,"<").replace(/>/g,">")}</div>`);emojiSearchResultsCtn.innerHTML=a.join("")+(a.length>0?'<div class="end-padding" style="flex-grow:1000000;pointer-events: none;opacity: 0;"></div>':""),emojiSearchResultsCtn.querySelectorAll("div:not(.end-padding)").forEach(e=>{e.addEventListener("click",emojiSearchResultClickHandler)})}emojiSearchModal.addEventListener("click",function(e){e.composedPath().includes(emojiSearchModalInner)||closeEmojiPicker()})</script>
134 135 136 <div id="tipsAndTricksEl" style="display:none; font-size:80%; max-width: 95vw;width: 600px;margin: 0 auto;margin-top: 1rem;background: white;border-radius: 3px;padding: 0.75rem;box-shadow: 0px 1px 1px #b4b4b4;"> 137 <div style="font-size: 0.9rem; text-align: center;font-weight: bold;">Tips:</div> 138 <ul style="margin-top: 0.25rem;padding-left: 2rem;margin-bottom: 0;"> 139 <li>Hold <b>Ctrl</b> and then click anywhere on the canvas to quickly switch your brush to the clicked emoji.</li> 140 <li style="margin-top: 0.25rem;">You might also like to check out our <a href="/dot-art-generator" target="_blank">image to dot art generator</a> and our <a href="/dot-art-editor" target="_blank">dot art editor</a>.</li> 141 </ul> 142 <div style="text-align:center; margin-top:0.5rem;"> 143 <button onclick='localStorage.alreadySeenEmojiArtEditorTips="v1",tipsAndTricksEl.style.display="none"'>Close</button> 144 </div> 145 </div> 146
146<script>"v1"!==localStorage.alreadySeenEmojiArtEditorTips&&(tipsAndTricksEl.style.display="")</script>
146 147 148 <p style="text-align:center; font-size:0.8rem; color:grey; max-width:500px; margin:0 auto; margin-top:1rem;">If you've found a bug or have feature ideas, you can <a style="color:inherit;" href="https://docs.google.com/forms/d/e/1FAIpQLSe4Sx-ol2SMbWO4isLP9xyweLXGte4-bvoUnyABk5dqO1CNSg/viewform?usp=sf_link" target="_blank">provide feedback here</a>.</p> 149 150
150<script>setInterval(()=>{!document.hidden&&window.userHasEditedArt&&(localStorage.emojiArtEditorText=gridEl.textContent)},1e4)</script>
150 151 152
152<script>window.currentEmoji="â¬",currentEmojiDisplayEl.innerHTML=window.currentEmoji;let hashData={};try{hashData=JSON.parse(decodeURIComponent(location.hash.slice(1))||"{}")}catch(t){console.error(t)}function handleMouseOrTouchMove(t){let e;if(t.touches){if(1!==t.touches.length)return;e=t.touches[0]}else{if(1!==t.buttons)return;e=t}let r=e.clientX,i=e.clientY,o=document.elementFromPoint(r,i);o.classList.contains("b")&&(t.preventDefault(),e.ctrlKey?updateCurrentEmoji(o.textContent):(o.textContent=window.currentEmoji,window.userHasEditedArt=!0))}function generateDefaultEmojiArtGrid(t,e){text="";for(let r=0;r<e;r++)text+="â¬".repeat(t)+(r===e-1?"":"\n");return text}function openEmojiPicker(){emojiSearchModal.style.display="flex",emojiSearchInputEl.focus()}function closeEmojiPicker(){emojiSearchModal.style.display="none"}function updateEmojiPalette(){currentPaletteListEl.innerHTML='<div style="display:inline-flex; align-items:center; justify-content:center; height:fill-available; padding:0.25rem;">Palette:</div>';let t=[...new Set(graphemeSplitter.splitGraphemes(gridEl.textContent.replace(/\s+/g,"")))];t.includes(window.currentEmoji)||t.unshift(window.currentEmoji);for(let e of t)currentPaletteListEl.innerHTML+=`<div style="margin-left:0.25rem; display:inline-flex; align-items:center; justify-content:center; padding:0.25rem;"><button onclick="updateCurrentEmoji(this.dataset.emoji);" data-emoji="${e}" style="padding: 0.25rem;">${e}</button></div>`}function updateCurrentEmoji(t){window.currentEmoji=t,currentEmojiDisplayEl.innerHTML=t}document.addEventListener("DOMContentLoaded",async function(){gridEl.innerHTML=stringToHTMLGrid(generateDefaultEmojiArtGrid(3,3)),await new Promise(t=>setTimeout(t,3));let t,e=gridEl.querySelector(".b").offsetHeight;gridEl.style.lineHeight=e+"px",hashData.text?localStorage.emojiArtEditorText?confirm("You have an existing drawing in your browser cache. Loading this drawing will erase it. Click 'OK' to continue, or 'Cancel' to load the drawing in your cache.")?(t=hashData.text,localStorage.emojiArtEditorText=hashData.text):t=localStorage.emojiArtEditorText:t=hashData.text:localStorage.emojiArtEditorText&&(t=localStorage.emojiArtEditorText),t&&!validateEmojiArtGrid(t)&&(alert("Attempted to initialize imported or cached emoji art, but it was corrupted (e.g. non-emoji characters). Will now try to uncorrupt, but the result may look distorted."),t=uncorruptEmojiArt(t),validateEmojiArtGrid(t)||(t=void 0));let r=!1;t||(t=generateDefaultEmojiArtGrid(15,15),r=!0),location.hash="",gridEl.innerHTML=stringToHTMLGrid(t),r||updateEmojiPalette(),recordHistoryStateIfChanged()}),window.addEventListener("keydown",function(t){(t.ctrlKey||t.metaKey)&&"KeyZ"===t.code&&(t.shiftKey?redo():undo()),(t.ctrlKey||t.metaKey)&&"KeyY"===t.code&&redo()}),window.addEventListener("mouseup",recordHistoryStateIfChanged),window.addEventListener("touchend",recordHistoryStateIfChanged),window.addEventListener("touchstart",handleMouseOrTouchMove,{passive:!1}),window.addEventListener("touchmove",handleMouseOrTouchMove,{passive:!1}),window.addEventListener("mousemove",handleMouseOrTouchMove,{passive:!1}),window.addEventListener("mousedown",handleMouseOrTouchMove,{passive:!1}),window.addEventListener("keydown",function(t){"Control"==t.key&&(gridEl.style.cursor="crosshair")}),window.addEventListener("keyup",function(t){"Control"==t.key&&(gridEl.style.cursor="default")});let stateHistoryArray=[],stateHistoryIndex=0;function recordHistoryStateIfChanged(){gridEl.textContent!==stateHistoryArray[stateHistoryIndex]&&(0!==stateHistoryIndex&&(stateHistoryArray=stateHistoryArray.slice(stateHistoryIndex),stateHistoryIndex=0),stateHistoryArray.unshift(gridEl.textContent),stateHistoryArray.length>100&&stateHistoryArray.pop())}function undo(){0===stateHistoryIndex&&recordHistoryStateIfChanged(),stateHistoryIndex<stateHistoryArray.length-1&&(stateHistoryIndex++,gridEl.innerHTML=stringToHTMLGrid(stateHistoryArray[stateHistoryIndex]))}function redo(){stateHistoryIndex>0&&(stateHistoryIndex--,gridEl.innerHTML=stringToHTMLGrid(stateHistoryArray[stateHistoryIndex]))}function importButtonClickHandler(){let t=prompt("Paste the emoji art that you wish to import into the text box below. (Caution: The existing emoji art on the canvas will be overwritten)");t&&(t&&!validateEmojiArtGrid(t)&&(alert("Attempted to initialize imported emoji art, but it was corrupted (e.g. non-emoji characters). Will now try to uncorrupt, but the result may look distorted."),t=uncorruptEmojiArt(t),validateEmojiArtGrid(t)||(t=void 0)),t||(t=generateDefaultEmojiArtGrid(15,15)),gridEl.innerHTML=stringToHTMLGrid(t),recordHistoryStateIfChanged())}function addPaddingClickHandler(){let t=prompt("The currently-selected emoji will be used for padding. Indicate how much padding to add to the top,right,bottom,left (i.e. clockwise from top) like so:","3,3,3,3");
152if(null===t)return;let e=t.split(",").map(t=>Number(t.trim())).filter(t=>!isNaN(t)&&t>=0);if(4!==e.length)return alert("You entered invalid padding numbers. You should enter numbers in the following format: top,right,bottom,left");let[r,i,o,n]=e,a=window.currentEmoji,l=gridEl.textContent.trim().split("\n"),s=graphemeSplitter.splitGraphemes(l[0]).length;for(let t=0;t<o;t++)l.push(a.repeat(s));for(let t=0;t<r;t++)l.unshift(a.repeat(s));for(let t=0;t<l.length;t++)l[t]=a.repeat(n)+l[t];for(let t=0;t<l.length;t++)l[t]=l[t]+a.repeat(i);gridEl.innerHTML=stringToHTMLGrid(l.join("\n")),recordHistoryStateIfChanged()}function cropButtonClickHandler(){let t=gridEl.textContent.trim();if(""===t.replace(/[â \n]+/g,"").trim())return alert("Cannot crop an empty grid.");gridEl.innerHTML=stringToHTMLGrid(cropEmojiArt(t)),t.length===gridEl.textContent.length&&alert("Tried to crop whitespace from top and sides, but there was none."),recordHistoryStateIfChanged()}function clearButtonClickHandler(){let t=gridEl.textContent.split("\n"),e=prompt("How big should the new grid be? Use the format 'width,height'. Caution: This will delete your existing drawing.",Math.max(graphemeSplitter.splitGraphemes(t[0]).length,3)+","+Math.max(t.length,3));if(!e)return;let[r,i]=e.split(",").map(t=>Number(t));if(!r||!i)return alert("Invalid width or height.");gridEl.innerHTML=stringToHTMLGrid(generateDefaultEmojiArtGrid(r,i)),recordHistoryStateIfChanged()}function cropEmojiArt(t){let e=t.split("\n"),r=graphemeSplitter.splitGraphemes(e[0].trim()),i=graphemeSplitter.splitGraphemes(e[e.length-1].trim()),o=[r[0],r[r.length-1],i[0],i[i.length-1]].reduce((t,e)=>(t[e]=(t[e]||0)+1,t),{}),n=Object.entries(o).sort((t,e)=>e[1]-t[1])[0][0],a=1/0;for(let t of e){let e=graphemeSplitter.splitGraphemes(t),r=0;for(;e[r]===n;)r++;r<a&&(a=r)}let l=1/0;for(let t of e){let e=graphemeSplitter.splitGraphemes(t),r=0;for(;e[e.length-1-r]===n;)r++;r<l&&(l=r)}for(e=e.map(t=>graphemeSplitter.splitGraphemes(t)).map(t=>t.slice(a,l?-l:t.length).join(""));0===graphemeSplitter.splitGraphemes(e[0]).filter(t=>t!==n).length;)e=e.slice(1);for(;0===graphemeSplitter.splitGraphemes(e[e.length-1]).filter(t=>t!==n).length;)e=e.slice(0,-1);return e.join("\n")}function zoomIn(){gridEl.style.fontSize=1.5*Number(gridEl.style.fontSize.slice(0,-2))+"px",gridEl.style.lineHeight=1.5*Number(gridEl.style.lineHeight.slice(0,-2))+"px"}function zoomOut(){gridEl.style.fontSize=Number(gridEl.style.fontSize.slice(0,-2))/1.5+"px",gridEl.style.lineHeight=Number(gridEl.style.lineHeight.slice(0,-2))/1.5+"px"}function stringToHTMLGrid(t){return graphemeSplitter.splitGraphemes(t).map(t=>`<span class="b">${t}</span>`).join("")}function openSaveTextWindow(t){cropEmojiArt(t).length<.7*t.length&&"yes"===prompt("Your art contains a lot of padding - would you like to crop away the padding before saving? (yes/no)","yes")&&(t=cropEmojiArt(t));let e={text:t};window.open(`https://emojicombos.com/submit#${encodeURIComponent(JSON.stringify(e))}`,"_blank");try{gtag("event","save_emoji_art_from_editor_click",{})}catch(t){console.error(t)}}function validateEmojiArtGrid(t){let e=t.trim().split("\n");if(0===e.length)return!1;let r=graphemeSplitter.splitGraphemes(e[0]).length;return!(e.filter(t=>graphemeSplitter.splitGraphemes(t).length!==r).length>0)}function uncorruptEmojiArt(t){let e=(t=(t=t.replace(/ {3,5}/g,"â¬")).replace(/ +/g,"")).split("\n").map(t=>graphemeSplitter.splitGraphemes(t));e=e.map(t=>t.map(t=>t.match(/(\p{Emoji}\uFE0F|\p{Emoji_Presentation})/gu)?t:"â¬"));let r=e.map(t=>t.length),i=r.sort()[Math.round(r.length/2)];return e=e.map(t=>t.length>i?t.slice(0,i):t.length<i?t+"â ".repeat(i-t.length):t),e.map(t=>t.join("")).join("\n").trim()}async function copyEmojiArtClickHandler(t){await copyTextToClipboard(t);try{gtag("event","copy_emoji_art_from_editor_click",{from_imported:void 0!==hashData.text})}catch(t){console.error(t)}}function fallbackCopyTextToClipboard(t){var e=document.createElement("textarea");e.value=t,e.style.top="0",e.style.left="0",e.style.position="fixed",document.body.appendChild(e),e.focus(),e.select();try{var r=document.execCommand("copy")?"successful":"unsuccessful";console.log("Copying text command was "+r)}catch(t){console.error("Fallback: Oops, unable to copy",t)}document.body.removeChild(e)}function copyTextToClipboard(t){navigator.clipboard?navigator.clipboard.writeText(t).then(function(){console.log("Async: Copying to clipboard was successful!")},function(t){console.error("Async: Could not copy text: ",t)}):fallbackCopyTextToClipboard(t)}function sleep(t){return new Promise(e=>setTimeout(e,t))}</script>
152 153 154 155 <!-- Global site tag (gtag.js) - Google Analytics -->
vendor: 68 bytes, lines 155-156
155 156 <script async src="https://www.googletagmanager.com/gtag/js?id=
156G-WF5V0J3NPY
vendor: 16 bytes, lines 156-157
156"></script> 157
157<script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-WF5V0J3NPY")</script>
157 158 159
159<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"b2a24a8737c948ef83556434bedb6c30","r":1,"spa":2}' crossorigin="anonymous"></script>
159 160</body> 161</html> 162
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.