Created
October 23, 2020 19:37
-
-
Save nhuynh1/6cfddbbc8be6bc087c9cb46617ef67bc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Canvas Images</title> | |
<link rel="stylesheet" href="style.css"> | |
<link href="https://fonts.googleapis.com/css?family=Amatic+SC&display=swap" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Permanent+Marker&display=swap" rel="stylesheet"> | |
</head> | |
<body> | |
<header> | |
<div class="logo-main"> | |
Remixoji | |
</div> | |
<div class="social-icons"> | |
<a href="#"><svg viewBox="0 0 512 512" width="24px" height="24px" xmlns="http://www.w3.org/2000/svg"><path d="m224.113281 303.960938 83.273438-47.960938-83.273438-47.960938zm0 0"/><path d="m256 0c-141.363281 0-256 114.636719-256 256s114.636719 256 256 256 256-114.636719 256-256-114.636719-256-256-256zm159.960938 256.261719s0 51.917969-6.585938 76.953125c-3.691406 13.703125-14.496094 24.507812-28.199219 28.195312-25.035156 6.589844-125.175781 6.589844-125.175781 6.589844s-99.878906 0-125.175781-6.851562c-13.703125-3.6875-24.507813-14.496094-28.199219-28.199219-6.589844-24.769531-6.589844-76.949219-6.589844-76.949219s0-51.914062 6.589844-76.949219c3.6875-13.703125 14.757812-24.773437 28.199219-28.460937 25.035156-6.589844 125.175781-6.589844 125.175781-6.589844s100.140625 0 125.175781 6.851562c13.703125 3.6875 24.507813 14.496094 28.199219 28.199219 6.851562 25.035157 6.585938 77.210938 6.585938 77.210938zm0 0"/></svg></a> | |
<a href="#"><svg viewBox="0 0 512 512" width="24px" height="24px" xmlns="http://www.w3.org/2000/svg"><path d="m305 256c0 27.0625-21.9375 49-49 49s-49-21.9375-49-49 21.9375-49 49-49 49 21.9375 49 49zm0 0"/><path d="m370.59375 169.304688c-2.355469-6.382813-6.113281-12.160157-10.996094-16.902344-4.742187-4.882813-10.515625-8.640625-16.902344-10.996094-5.179687-2.011719-12.960937-4.40625-27.292968-5.058594-15.503906-.707031-20.152344-.859375-59.402344-.859375-39.253906 0-43.902344.148438-59.402344.855469-14.332031.65625-22.117187 3.050781-27.292968 5.0625-6.386719 2.355469-12.164063 6.113281-16.902344 10.996094-4.882813 4.742187-8.640625 10.515625-11 16.902344-2.011719 5.179687-4.40625 12.964843-5.058594 27.296874-.707031 15.5-.859375 20.148438-.859375 59.402344 0 39.25.152344 43.898438.859375 59.402344.652344 14.332031 3.046875 22.113281 5.058594 27.292969 2.359375 6.386719 6.113281 12.160156 10.996094 16.902343 4.742187 4.882813 10.515624 8.640626 16.902343 10.996094 5.179688 2.015625 12.964844 4.410156 27.296875 5.0625 15.5.707032 20.144532.855469 59.398438.855469 39.257812 0 43.90625-.148437 59.402344-.855469 14.332031-.652344 22.117187-3.046875 27.296874-5.0625 12.820313-4.945312 22.953126-15.078125 27.898438-27.898437 2.011719-5.179688 4.40625-12.960938 5.0625-27.292969.707031-15.503906.855469-20.152344.855469-59.402344 0-39.253906-.148438-43.902344-.855469-59.402344-.652344-14.332031-3.046875-22.117187-5.0625-27.296874zm-114.59375 162.179687c-41.691406 0-75.488281-33.792969-75.488281-75.484375s33.796875-75.484375 75.488281-75.484375c41.6875 0 75.484375 33.792969 75.484375 75.484375s-33.796875 75.484375-75.484375 75.484375zm78.46875-136.3125c-9.742188 0-17.640625-7.898437-17.640625-17.640625s7.898437-17.640625 17.640625-17.640625 17.640625 7.898437 17.640625 17.640625c-.003906 9.742188-7.898437 17.640625-17.640625 17.640625zm0 0"/><path d="m256 0c-141.363281 0-256 114.636719-256 256s114.636719 256 256 256 256-114.636719 256-256-114.636719-256-256-256zm146.113281 316.605469c-.710937 15.648437-3.199219 26.332031-6.832031 35.683593-7.636719 19.746094-23.246094 35.355469-42.992188 42.992188-9.347656 3.632812-20.035156 6.117188-35.679687 6.832031-15.675781.714844-20.683594.886719-60.605469.886719-39.925781 0-44.929687-.171875-60.609375-.886719-15.644531-.714843-26.332031-3.199219-35.679687-6.832031-9.8125-3.691406-18.695313-9.476562-26.039063-16.957031-7.476562-7.339844-13.261719-16.226563-16.953125-26.035157-3.632812-9.347656-6.121094-20.035156-6.832031-35.679687-.722656-15.679687-.890625-20.6875-.890625-60.609375s.167969-44.929688.886719-60.605469c.710937-15.648437 3.195312-26.332031 6.828125-35.683593 3.691406-9.808594 9.480468-18.695313 16.960937-26.035157 7.339844-7.480469 16.226563-13.265625 26.035157-16.957031 9.351562-3.632812 20.035156-6.117188 35.683593-6.832031 15.675781-.714844 20.683594-.886719 60.605469-.886719s44.929688.171875 60.605469.890625c15.648437.710937 26.332031 3.195313 35.683593 6.824219 9.808594 3.691406 18.695313 9.480468 26.039063 16.960937 7.476563 7.34375 13.265625 16.226563 16.953125 26.035157 3.636719 9.351562 6.121094 20.035156 6.835938 35.683593.714843 15.675781.882812 20.683594.882812 60.605469s-.167969 44.929688-.886719 60.605469zm0 0"/></svg></a> | |
</div> | |
</header> | |
<div class="remix-wrap"> | |
<div id="options-tabs" class="tabs-container"> | |
<div class="options"></div> | |
</div> | |
<div class="remix-area"> | |
<svg id="remix" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"></svg> | |
<div class="download-wrap"> | |
<a class="btn" id="download-btn" onclick="downloadRaster()">Download</a><a class="btn dropbtn" id="download-extras-btn"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> | |
<path fill="#000000" d="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z"></path></svg></a> | |
<div class="dropdown"> | |
<div class="dropdown-content" id="download-extras"> | |
<a onclick="downloadRaster()">PNG (1080 x 1080)</a> | |
<a onclick="downloadRaster('jpg')">JPG (1080 x 1080)</a> | |
<a onclick="downloadSVG()">SVG</a> | |
</div> | |
</div> | |
</div> | |
<div class="randomize"></div> | |
</div> | |
</div> | |
<div class="button-wrap"> | |
<button onclick="clearSVG()">Clear</button> | |
</div> | |
<div class="tools-wrap"> | |
<div class="license"> | |
<p>License: Creative Commons</p> | |
<p>Code: Github</p> | |
<p>Emoji Graphics: Twitter Open Source Emojis</p> | |
</div> | |
</div> | |
<div class="social-wrap"> | |
<span>Made with 💛 in Toronto</span> | |
</div> | |
<script> | |
/* Template */ | |
const optionsCategoryTemplate = (category) => { | |
let div = document.createElement('div'), | |
h2 = document.createElement('h2'); | |
div.classList.add('category'); | |
// h2.textContent = category; | |
// div.appendChild(h2); | |
return div; | |
} | |
const clearPartButton = (partType) => { | |
let button = document.createElement('button'); | |
button.textContent = `Remove ${partType}`; | |
button.classList.add('clear'); | |
button.dataset.partType = partType; | |
return button; | |
} | |
const tabsTemplate = (category) => { | |
let li = document.createElement('li'); | |
li.setAttribute('role', 'tab'); | |
li.setAttribute('tabindex', '0'); | |
li.textContent = category; | |
return li; | |
} | |
const tabsListTemplate = () => { | |
let ul = document.createElement('ul'); | |
ul.setAttribute('aria-controls', 'options-tabs'); | |
ul.setAttribute('role', 'tablist'); | |
return ul; | |
} | |
/* Functions */ | |
const empty = (parent) => { | |
while(parent.lastChild) parent.lastChild.remove(); | |
} | |
const createURI = (remixSVG) => 'data:image/svg+xml,' + encodeURIComponent(remixSVG.outerHTML); | |
const removePartType = (remixSVG, partType) => { | |
const partToRemove = remixSVG.querySelector(`[data-part-type="${partType}"]`); | |
if(partToRemove) remixSVG.removeChild(partToRemove); | |
} | |
const textToShapes = async (text, svg = true) => { | |
const parser = new DOMParser(), | |
xml = parser.parseFromString(text, 'image/svg+xml'); | |
return svg ? xml.querySelector('svg') : | |
xml.querySelector('svg').childNodes; | |
} | |
const getShapeData = async ({ src, partType }) => { | |
try { | |
let response = await fetch(src); | |
let text = await response.text(); | |
let shapesNodeList = await textToShapes(text, false); | |
return { src, shapesNodeList, partType }; | |
} | |
catch (e) { | |
console.error(e); | |
return false; | |
} | |
} | |
const insertShapeNode = ({ shapesNodeList, partType }) => { | |
if(!shapesNodeList) return; | |
removePartType(remixSVG, partType); | |
const g = Array.from(shapesNodeList).reduce((g, node) => { | |
g.appendChild(node); | |
return g; | |
}, document.createElementNS('http://www.w3.org/2000/svg', 'g')); | |
g.dataset.partType = partType; | |
if(partType === "face") { | |
remixSVG.insertBefore(g, remixSVG.firstElementChild); | |
} else { | |
remixSVG.appendChild(g); | |
} | |
} | |
const loadEmoji = ({ src, partType }) => { | |
getShapeData({ src, partType }) | |
.then(insertShapeNode); | |
} | |
const randomizeParts = (partsByCategoryObj) => { | |
const categories = Object.keys(partsByCategoryObj); | |
const randomParts = categories.map(category => { | |
let categoryArray = partsByCategoryObj[category]; | |
let randomIndex = Math.floor(Math.random() * categoryArray.length); | |
return categoryArray[randomIndex]; | |
}); | |
return randomParts; | |
} | |
const domain = 'http://localhost:8888/'; | |
const imageFolder = 'emojiparts/'; | |
const url = domain + 'emojiparts.json'; | |
const optionsMain = document.querySelector('.options'); | |
const remixSVG = document.querySelector('#remix'); | |
const optionsTabs = document.querySelector('#options-tabs'); | |
const randomButtonContainer = document.querySelector('.randomize'); | |
let partsByCategoryObj; | |
const showRandomEmoji = () => { | |
const randomParts = randomizeParts(partsByCategoryObj); | |
randomParts.forEach(part => loadEmoji({src: domain + imageFolder + part.filename, partType: part.part})); | |
randomButtonContainer.style.visibility = 'hidden'; | |
} | |
const fetchParts = async (url) => { | |
let response = await fetch(url); | |
return await response.json(); | |
} | |
const categorizeParts = async (json) => { | |
const parts = [...json]; | |
return parts.reduce((categorized, part) => { | |
categorized[part.part] = categorized[part.part] ? | |
[...categorized[part.part], part] : | |
[part]; | |
return categorized; | |
},{}); | |
} | |
const insertPartsAsImage = async (partsByCategory) => { | |
const categories = Object.keys(partsByCategory); | |
const tabList = tabsListTemplate(); | |
categories.forEach((category, index) => { | |
const parts = partsByCategory[category]; | |
// dynamically create the tabs here | |
const tab = tabsTemplate(category); | |
tab.setAttribute('aria-controls', `options-tabs_${index}`); | |
tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); | |
const categoryDiv = optionsCategoryTemplate(category); | |
categoryDiv.id = `options-tabs_${index}`; | |
categoryDiv.setAttribute('role', 'tabpanel'); | |
categoryDiv.setAttribute('aria-expanded', index === 0 ? 'true' : 'false'); | |
const fragment = parts.reduce((fragment, part) =>{ | |
const img = new Image; | |
img.src = `${domain}${imageFolder}${part.filename}`; | |
img.dataset.partType = part.part; | |
fragment.appendChild(img); | |
return fragment; | |
}, document.createDocumentFragment()); | |
fragment.insertBefore(clearPartButton(category), fragment.firstElementChild) | |
categoryDiv.appendChild(fragment); | |
optionsMain.appendChild(categoryDiv); | |
tabList.appendChild(tab); | |
}); | |
optionsTabs.insertBefore(tabList, optionsMain); | |
return partsByCategory; | |
} | |
const enableRandomization = (partsByCategory) => { | |
partsByCategoryObj = partsByCategory; | |
const randomizeButton = document.createElement('button'); | |
// randomizeButton.textContent = 'Randomize'; | |
randomizeButton.setAttribute('onclick', 'showRandomEmoji()'); | |
randomButtonContainer.appendChild(randomizeButton); | |
} | |
fetchParts(url) | |
.then(categorizeParts) | |
.then(insertPartsAsImage) | |
.then(enableRandomization); | |
const clearSVG = () => { | |
// removeAllChildren(remixSVG); | |
empty(remixSVG); | |
randomButtonContainer.style.visibility = 'visible'; | |
} | |
const clearButtonClickHandler = (e) => { | |
const partType = e.target.dataset.partType; | |
removePartType(remixSVG, partType); | |
} | |
const emojiPartsClickHandler = (e) => { | |
const selectedPart = e.target, | |
selectedPartTag = selectedPart.tagName; | |
switch(selectedPartTag){ | |
case 'IMG': | |
randomButtonContainer.style.visibility = 'hidden'; | |
const src = selectedPart.src, | |
partType = selectedPart.dataset.partType; | |
loadEmoji({ src, partType }); | |
break; | |
case 'BUTTON': | |
clearButtonClickHandler(e); | |
break; | |
default: | |
return; | |
} | |
} | |
optionsMain.addEventListener('click', emojiPartsClickHandler); | |
/* Downloads */ | |
// https://stackoverflow.com/questions/53188714/convert-svg-to-png-jpeg-with-custom-width-and-height | |
// https://stackoverflow.com/questions/28226677/save-inline-svg-as-jpeg-png-svg | |
const downloadRaster = (fileType = "png") => { | |
const svgString = (new XMLSerializer()).serializeToString(remixSVG); | |
const svgEncode = btoa(svgString); | |
const img = new Image(); | |
img.width = 800; | |
img.height = 800; | |
img.src = 'data:image/svg+xml;base64,' + svgEncode; | |
img.onload = () => { | |
const canvas = document.createElement('canvas'), | |
ctx = canvas.getContext('2d'); | |
canvas.width = 1080; | |
canvas.height = 1080; | |
if(['jpeg', 'jpg'].includes(fileType)) { | |
ctx.fillStyle = 'white'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
} | |
ctx.drawImage(img, (canvas.width - img.width) / 2, (canvas.height - img.height) / 2, img.width, img.height); | |
let link; | |
switch(fileType){ | |
case 'png': | |
link = canvas.toDataURL(); | |
break; | |
case 'jpg': | |
link = canvas.toDataURL('image/jpeg', 1.0); | |
break; | |
case 'jpeg': | |
link = canvas.toDataURL('image/jpeg', 1.0); | |
break; | |
default: | |
console.log(`${fileType} is not a supported option`); | |
} | |
const a = document.createElement('a'); | |
a.href = link; | |
a.download = `remixedEmoji.${fileType}`; | |
a.style.display = 'none'; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
} | |
// const downloadPNG = () => { | |
// const svgString = (new XMLSerializer()).serializeToString(remixSVG); | |
// const svgEncode = btoa(svgString); | |
// | |
// const img = new Image(); | |
// img.width = 800; | |
// img.height = 800; | |
// img.src = 'data:image/svg+xml;base64,' + svgEncode; | |
// | |
// img.onload = () => { | |
// const canvas = document.createElement('canvas'), | |
// ctx = canvas.getContext('2d'); | |
// | |
// canvas.width = 1080; | |
// canvas.height = 1080; | |
// ctx.drawImage(img, 140, 140, 800, 800); | |
// const link = canvas.toDataURL(); | |
// | |
// const a = document.createElement('a'); | |
// a.href = link; | |
// a.download = 'remixedEmoji.png'; | |
// a.style.display = 'none'; | |
// document.body.appendChild(a); | |
// a.click(); | |
// document.body.removeChild(a); | |
// } | |
// } | |
const downloadSVG = () => { | |
const svgURI = createURI(remixSVG); | |
const a = document.createElement('a'); | |
a.href = svgURI; | |
a.download = 'remixedEmoji.svg'; | |
a.style.display = 'none'; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
/* Tabs */ | |
const tabContainer = document.getElementById('options-tabs'); | |
const showTab = (id) => { | |
const tabs = tabContainer.querySelectorAll('[role="tab"]'), | |
tabPanels = tabContainer.querySelectorAll('[role="tabpanel"]'), | |
selectedTab = tabContainer.querySelector(`[aria-controls="${id}"]`), | |
selectedTabpanel = tabContainer.querySelector(`#${id}`); | |
tabs.forEach(tab => tab.setAttribute('aria-selected', 'false')); | |
tabPanels.forEach(tabpanel => tabpanel.setAttribute('aria-expanded', 'false')); | |
selectedTab.setAttribute("aria-selected", "true"); | |
selectedTab.focus(); | |
selectedTabpanel.setAttribute("aria-expanded", "true"); | |
} | |
const tabContainerClickHandler = (e) => { | |
e.preventDefault(); | |
if(e.target.getAttribute('role') !== 'tab') return; | |
showTab(e.target.getAttribute('aria-controls')); | |
} | |
tabContainer.addEventListener('click', tabContainerClickHandler); | |
/* Download dropdown */ | |
const downloadFile = () => { | |
console.log('download'); | |
} | |
const downloadExtras = document.getElementById('download-extras'); | |
const downloadExtrasButton = document.getElementById('download-extras-btn'); | |
downloadExtrasButton.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
const closeDownloadExtras = (e) => { | |
downloadExtras.classList.remove('open'); | |
document.body.removeEventListener('click', closeDownloadExtras); | |
} | |
if(downloadExtras.classList.contains('open')){ | |
downloadExtras.classList.remove('open'); | |
document.body.removeEventListener('click', closeDownloadExtras); | |
} | |
else { | |
downloadExtras.classList.add('open'); | |
document.body.addEventListener('click', closeDownloadExtras); | |
} | |
}); | |
/* SVG Drag */ | |
let selectedPart, offset, transform; | |
const getMousePosition = ({ clientX, clientY }) => { | |
const CTM = remixSVG.getScreenCTM(); | |
return { x: (clientX - CTM.e) / CTM.a, y: (clientY - CTM.f) / CTM.d } | |
} | |
const mouseDownHandler = (e) => { | |
if(e.target.matches('svg')) return; | |
selectedPart = e.target.parentElement.matches('g') ? | |
e.target.parentElement : e.target; | |
offset = getMousePosition(e); | |
const transforms = selectedPart.transform.baseVal; | |
if (transforms.length === 0 || | |
transforms.getItem(0).type !== SVGTransform.SVG_TRANSFORM_TRANSLATE) { | |
const translate = remixSVG.createSVGTransform(); | |
translate.setTranslate(0,0); | |
selectedPart.transform.baseVal.insertItemBefore(translate, 0); | |
} | |
transform = transforms.getItem(0); | |
offset.x -= transform.matrix.e; | |
offset.y -= transform.matrix.f; | |
} | |
const mouseMoveHandler = (e) => { | |
if(selectedPart) { | |
e.preventDefault(); | |
const coords = getMousePosition(e); | |
transform.setTranslate(coords.x - offset.x, coords.y - offset.y); | |
} | |
} | |
const mouseUpHandler = (e) => { | |
selectedPart = undefined; | |
} | |
remixSVG.addEventListener('mousedown', mouseDownHandler); | |
remixSVG.addEventListener('mousemove', mouseMoveHandler); | |
remixSVG.addEventListener('mouseup', mouseUpHandler); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment