Skip to content

Instantly share code, notes, and snippets.

@mrpelz
Last active January 24, 2024 23:24
Show Gist options
  • Save mrpelz/cd028441f2f22ea2c4813130539c2aca to your computer and use it in GitHub Desktop.
Save mrpelz/cd028441f2f22ea2c4813130539c2aca to your computer and use it in GitHub Desktop.
antifa.s3lph.me
<!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>
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