Last active
January 24, 2024 23:24
-
-
Save mrpelz/cd028441f2f22ea2c4813130539c2aca to your computer and use it in GitHub Desktop.
antifa.s3lph.me
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> | |
<head> | |
<style> | |
body { | |
font-family: sans-serif; | |
} | |
#form { | |
display: grid; | |
grid-template-columns: 30% 50%; | |
gap: 1ch; | |
input[type="checkbox"] { | |
width: fit-content; | |
} | |
label { | |
display: contents; | |
} | |
} | |
#svg { | |
background: repeating-conic-gradient(#dddddd 0% 25%, #999999 0% 50%) 50% / 20px 20px; | |
} | |
.download { | |
display: block; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Antifa Sticker Generator</h1> | |
<p> | |
A purely client-side sticker generator. | |
<a href="https://git.kabelsalat.ch/s3lph/antifa-sticker-generator">Source Code.</a> | |
</p> | |
<hr> | |
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500" id="svg"> | |
<style> | |
text { | |
fill: white; | |
font-family: sans-serif; | |
font-weight: bold; | |
font-size: 40px; | |
text-align: center; | |
text-anchor: middle; | |
} | |
</style> | |
<defs> | |
<filter id="filter-black"> | |
<feColorMatrix in="SourceGraphic" type="matrix" values="1 1 1 1 0 | |
0 0 0 0 0 | |
0 0 0 0 0 | |
0 0 0 1 0" /> | |
</filter> | |
<filter id="filter-red"> | |
<feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 | |
0 1 0 0 0 | |
0 0 1 0 0 | |
0 0 0 1 0" /> | |
</filter> | |
<path id="text-path-upper" stroke="red" fill="none" d="M 40 250 A 210 210 0 0 1 460 250" /> | |
<path id="text-path-lower" stroke="red" fill="none" d="M 10 250 A 230 230 0 0 0 490 250" /> | |
</defs> | |
<g id="svgroot" transform="scale(1, 1)"> | |
<circle id="bleed" cx="250" cy="250" r="249" stroke="#ff00ff" fill="none" stroke-width="0" /> | |
<circle cx="250" cy="250" r="225" stroke="black" fill="white" stroke-width="50" /> | |
<g id="icon"> | |
<image transform="translate(0 0) translate(-9 -2) scale(1, 1)" transform-origin="center" id="icon-a" width="512" | |
height="512" /> | |
<image transform="translate(0 0) translate(-9 -2) scale(1, 1)" transform-origin="center" id="icon-b" width="512" | |
height="512" /> | |
</g> | |
<text> | |
<textPath id="text-upper" startOffset="50%" href="#text-path-upper"></textPath> | |
</text> | |
<text> | |
<textPath id="text-lower" startOffset="50%" href="#text-path-lower"></textPath> | |
</text> | |
</g> | |
</svg> | |
<hr> | |
<form id="form"> | |
<label>Upper text | |
<input type="text" name="text-upper"> | |
</label> | |
<label>Lower text | |
<input type="text" name="text-lower"> | |
</label> | |
<label>Upper case | |
<input type="checkbox" name="text-uppercase" checked="true"> | |
</label> | |
<label>X Position | |
<input type="range" name="position-x" value="-9" min="-500" max="500"> | |
</label> | |
<label>Y Position | |
<input type="range" name="position-y" value="-2" min="-500" max="500"> | |
</label> | |
<label>X Shift | |
<input type="range" name="shift-x" value="0" min="-250" max="250"> | |
</label> | |
<label>Y Shift | |
<input type="range" name="shift-y" value="0" min="-250" max="250"> | |
</label> | |
<label>Black Scale | |
<input type="range" name="scale-black" value="0" min="-3" max="3" step="0.01"> | |
</label> | |
<label>Red Scale | |
<input type="range" name="scale-red" value="0" min="-3" max="3" step="0.01"> | |
</label> | |
<label>Same scale for black and red | |
<input type="checkbox" name="scale-lock"> | |
</label> | |
<label>Black Icon (black+white+alpha only) | |
<input type="file" name="icon-a" accept="image/*"> | |
</label> | |
<label>Red Icon (black+white+alpha only) | |
<input type="file" name="icon-b" accept="image/*"> | |
</label> | |
<label>Red on top of black | |
<input type="checkbox" name="color-swap" checked="true"> | |
</label> | |
<label>Bleed (black in download) | |
<input type="range" name="bleed" value="0" min="0" max="50"> | |
</label> | |
<label>PNG Pixel size | |
<input type="number" name="png-size" value="2048"> | |
</label> | |
</form> | |
<hr> | |
<div> | |
<a class="download" id="download-svg">Download SVG</a> | |
<a class="download" id="download-png">Download PNG</a> | |
</div> | |
<script src="main.js"></script> | |
</body> | |
</html> |
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
const defaultTextUpper = 'antifaschistische'; | |
const defaultTextLower = 'aktion'; | |
const defaultIconA = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNTEycHgiCiAgICAgaGVpZ2h0PSI1MTJweCIgdmlld0JveD0iMCAwIDUxMiA1MTIiPgogIDxwYXRoIGZpbGw9IiMwMDAwMDAiIGQ9Ik02OS45MTcsMjUxLjI1YzAsMCw0MS4wODQsMzguNDE2LDEwMi40MTcsNDYuMDgzYzYxLjMzMiw3LjY2Nyw2Ny4wMzktMTQuNDkxLDEwMC45MTYtMTQuMjUKICAgICAgICBjMjEuNTQ4LDAuMTUzLDI5LjMzNCw5LjU4NCwyOS4zMzQsOS41ODRsLTUzLjUwNiwxNDkuNjdjMCwwLTQuMTgtMC4wNDQtMTAuMDkyLTAuNjkyYy01LjkxMS0wLjY0OC05LjU0MS0xLjI5NC05LjU0MS0xLjI5NAogICAgICAgIEwyNTcsMzU3LjVjMCwwLTEzLjU4NC04LjcwNy0yNy4yNS0xMC41Yy0xMy42NjgtMS43OTMtMjEuMDQyLDMtNDYuNSwyLjVjLTI0LjA3My0wLjQ3My03MS4yNS0xMi43NS05My4zMzMtNDUuNzUKICAgICAgICBTNjkuOTE3LDI1MS4yNSw2OS45MTcsMjUxLjI1eiIvPgo8L3N2Zz4K'; | |
const defaultIconB = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNTEycHgiCiAgICAgaGVpZ2h0PSI1MTJweCIgdmlld0JveD0iMCAwIDUxMiA1MTIiPgogIDxwYXRoIGZpbGw9IiMwMDAwMDAiIGQ9Ik0xMjUuNjY3LDEyMi45MTdjMCwwLDI4LDMzLjA4Myw4OS4yNSwzNC4zMzNjNjEuMjUxLDEuMjUsOTUuOTE3LTM3LjkxNywxMzAuOTE3LTMzLjMzM3M2Myw0My4zMzMsNzcuNzUsNTAuMTY3TDMzMyw0MjUuNzUKICAgICAgICBjMCwwLTEuODMzLDEuMzM0LTExLDQuNjY3cy0xMS4zMzMsNC4zMzMtMTEuMzMzLDQuMzMzbDQwLjI1LTEyMi45MTdjMCwwLTM4Ljc1LTMyLjY4Ny03My43NS0zNQogICAgICAgIGMtMzUuMDAxLTIuMzEzLTUwLjkxNywyMy43NS0xMTAuMDgzLDEzLjc1QzEwNy45MTcsMjgwLjU4Myw2OS41LDI0My41LDY5LjUsMjQzLjVzLTEuNDE1LTIxLjA2NywxNC41LTU5LjgzMwogICAgICAgIEM5OS45MTQsMTQ0LjkwMiwxMjUuNjY3LDEyMi45MTcsMTI1LjY2NywxMjIuOTE3eiIvPgo8L3N2Zz4K'; | |
const svgFilterBlack = 'url(#filter-black)'; | |
const svgFilterRed = 'url(#filter-red)'; | |
const xmlSerializer = new XMLSerializer(); | |
/** | |
* @param {File | null} input | |
* @returns {Promise<string | undefined>} | |
*/ | |
const dataUrlFromFile = async (input) => { | |
if (!input?.size) return undefined; | |
const reader = new FileReader(); | |
/** | |
* @type {Promise<string>} | |
*/ | |
const result = new Promise((resolve, reject) => { | |
reader.addEventListener('load', () => { | |
resolve(reader.result); | |
}); | |
reader.addEventListener('error', () => reject()); | |
reader.readAsDataURL(input); | |
}); | |
return result; | |
}; | |
(() => { | |
/** | |
* @type {SVGElement | null} | |
*/ | |
const svg = document.querySelector('#svg'); | |
if (!svg) return; | |
/** | |
* @type {SVGTextPathElement | null} | |
*/ | |
const svgTextElementUpper = svg.querySelector('#text-upper'); | |
if (!svgTextElementUpper) return; | |
/** | |
* @type {SVGTextPathElement | null} | |
*/ | |
const svgTextElementLower = svg.querySelector('#text-lower'); | |
if (!svgTextElementLower) return; | |
/** | |
* @type {SVGImageElement | null} | |
*/ | |
const svgIconA = svg.querySelector('#icon-a'); | |
if (!svgIconA) return; | |
/** | |
* @type {SVGImageElement | null} | |
*/ | |
const svgIconB = svg.querySelector('#icon-b'); | |
if (!svgIconB) return; | |
/** | |
* @type {SVGCircleElement | null} | |
*/ | |
const svgBleed = svg.querySelector('#bleed'); | |
if (!svgBleed) return; | |
/** | |
* @type {HTMLFormElement | null} | |
*/ | |
const form = document.querySelector('#form'); | |
if (!form) return; | |
/** | |
* @type {HTMLInputElement | null} | |
*/ | |
const inputScaleRed = form.querySelector('input[name="scale-red"'); | |
if (!inputScaleRed) return; | |
/** | |
* @type {HTMLAnchorElement | null} | |
*/ | |
const downloadSVG = document.querySelector('#download-svg'); | |
if (!downloadSVG) return; | |
/** | |
* @type {HTMLAnchorElement | null} | |
*/ | |
const downloadPNG = document.querySelector('#download-png'); | |
if (!downloadPNG) return; | |
(() => { | |
/** | |
* @type {HTMLInputElement | null} | |
*/ | |
const inputTextUpper = form.querySelector('input[name="text-upper"'); | |
if (!inputTextUpper) return; | |
/** | |
* @type {HTMLInputElement | null} | |
*/ | |
const inputTextLower = form.querySelector('input[name="text-lower"'); | |
if (!inputTextLower) return; | |
inputTextUpper.value = defaultTextUpper; | |
inputTextLower.value = defaultTextLower; | |
})(); | |
const render = async () => { | |
const formData = new FormData(form); | |
console.log(Object.fromEntries(formData.entries())); | |
try { | |
// text | |
const textUppercase = formData.get('text-uppercase') === 'on'; | |
const textUpper_ = formData.get('text-upper'); | |
const textUpper = textUpper_?.length ? textUpper_ : defaultTextUpper; | |
svgTextElementUpper.textContent = textUppercase ? textUpper.toLocaleUpperCase() : textUpper; | |
const textLower_ = formData.get('text-lower'); | |
const textLower = textLower_?.length ? textLower_ : defaultTextLower; | |
svgTextElementLower.textContent = textUppercase ? textLower.toLocaleUpperCase() : textLower; | |
// position | |
const positionX = Number.parseInt(formData.get('position-x')); | |
svgIconA.transform.baseVal[1].matrix.e = positionX; | |
svgIconB.transform.baseVal[1].matrix.e = positionX; | |
const positionY = Number.parseInt(formData.get('position-y')); | |
svgIconA.transform.baseVal[1].matrix.f = positionY; | |
svgIconB.transform.baseVal[1].matrix.f = positionY; | |
// shift | |
const shiftX = Number.parseInt(formData.get('shift-x')); | |
svgIconA.transform.baseVal[0].matrix.e = shiftX; | |
svgIconB.transform.baseVal[0].matrix.e = -shiftX; | |
const shiftY = Number.parseInt(formData.get('shift-y')); | |
svgIconA.transform.baseVal[0].matrix.f = shiftY; | |
svgIconB.transform.baseVal[0].matrix.f = -shiftY; | |
// scale | |
const scaleLock = formData.get('scale-lock') === 'on'; | |
const scaleBlack_ = Number.parseFloat(formData.get('scale-black')); | |
const scaleBlack = Math.pow(10, scaleBlack_); | |
inputScaleRed.disabled = scaleLock; | |
if (scaleLock) inputScaleRed.value = scaleBlack_.toString(10); | |
svgIconA.transform.baseVal[2].matrix.a = scaleBlack; | |
svgIconA.transform.baseVal[2].matrix.d = scaleBlack; | |
const scaleRed = (() => { | |
if (scaleLock) return scaleBlack; | |
const scaleRed_ = Number.parseFloat(formData.get('scale-red')); | |
if (Number.isNaN(scaleRed_)) return scaleBlack; | |
return Math.pow(10, scaleRed_); | |
})(); | |
svgIconB.transform.baseVal[2].matrix.a = scaleRed; | |
svgIconB.transform.baseVal[2].matrix.d = scaleRed; | |
// icons | |
/** | |
* @type {File | null} | |
*/ | |
const iconA = await dataUrlFromFile(formData.get('icon-a')); | |
svgIconA.href.baseVal = iconA ?? defaultIconA; | |
/** | |
* @type {File | null} | |
*/ | |
const iconB = await dataUrlFromFile(formData.get('icon-b')); | |
svgIconB.href.baseVal = iconB ?? defaultIconB; | |
// color-swap | |
const colorSwap = formData.get('color-swap') === 'on'; | |
svgIconA.setAttribute('filter', colorSwap ? svgFilterRed : svgFilterBlack); | |
svgIconB.setAttribute('filter', colorSwap ? svgFilterBlack : svgFilterRed); | |
// bleed | |
const bleed = Number.parseInt(formData.get('bleed')); | |
const size = ((bleed * 2) + 500).toString(10); | |
svg.setAttribute('width', size); | |
svg.setAttribute('height', size); | |
svg.setAttribute('viewBox', `${-bleed} ${-bleed} ${size} ${size}`); | |
svgBleed.style.strokeWidth = bleed ? ((bleed * 2) + 1).toString(10) : '0'; | |
// download | |
/** | |
* @type {SVGElement} | |
*/ | |
const clonedSVG = svg.cloneNode(true); | |
/** | |
* @type {SVGCircleElement | null} | |
*/ | |
const clonedBleed = clonedSVG.querySelector('#bleed'); | |
if (clonedBleed) clonedBleed.setAttribute('stroke', 'black'); | |
const svgBlob = new Blob([xmlSerializer.serializeToString(clonedSVG)], { type: 'image/svg+xml' }) | |
if (downloadSVG.href) URL.revokeObjectURL(downloadSVG.href); | |
downloadSVG.href = URL.createObjectURL(svgBlob); | |
downloadSVG.download = `${textUpper}-${textLower}.svg`; | |
const pngSize_ = Number.parseInt(formData.get('png-size')); | |
const pngSize = Number.isNaN(pngSize_) ? 2048 : pngSize_; | |
const canvas = document.createElement('canvas'); | |
canvas.width = pngSize; | |
canvas.height = pngSize; | |
const context = canvas.getContext('2d'); | |
const rasterized = new Image(); | |
rasterized.addEventListener('load', () => { | |
context.drawImage(rasterized, 0, 0, pngSize, pngSize); | |
downloadPNG.href = canvas.toDataURL(); | |
downloadPNG.download = `${textUpper}-${textLower}.png`; | |
}); | |
rasterized.src = await dataUrlFromFile(svgBlob); | |
} catch (error) { | |
console.error(`Error applying values: ${error}`); | |
} | |
}; | |
render(); | |
form.addEventListener('input', render); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment