Skip to content

Instantly share code, notes, and snippets.

@nhuynh1
Created October 23, 2020 19:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhuynh1/6cfddbbc8be6bc087c9cb46617ef67bc to your computer and use it in GitHub Desktop.
Save nhuynh1/6cfddbbc8be6bc087c9cb46617ef67bc to your computer and use it in GitHub Desktop.
<!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