691 lines
No EOL
22 KiB
HTML
691 lines
No EOL
22 KiB
HTML
<!DOCTYPE html>
|
|
<head>
|
|
<title>Open Signature Generator</title>
|
|
<meta name="Open Signature Generator" />
|
|
<meta name="description" content="Generate and verify document signatures for free!" />
|
|
<meta name="keywords" content="open signature generator, signature, sign, digital signature, electronic signature, esign, e-signature, e-sign, pdf, qrcode, open source, free, privacy, docusign, adobe acrobat" />
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
|
</head>
|
|
<body>
|
|
<div id="title-container">
|
|
<h1>
|
|
Open Signature Generator
|
|
</h1>
|
|
</div>
|
|
|
|
<div id="document-container">
|
|
<div class="document-content">
|
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
|
<p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.</p>
|
|
</div>
|
|
|
|
<div class="signature-area">
|
|
<img src="example-signature.webp" class="signature-image" alt="Signature" />
|
|
<img src="example-signature-inverted.webp" class="signature-image signature-image-inverted hidden" alt="Signature" />
|
|
<div id="signature-line"></div>
|
|
<svg class="copy-hint-arrow hidden" viewBox="0 -2 16 18">
|
|
<path fill-rule="evenodd" d="M14.854 4.854a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 4H3.5A2.5 2.5 0 0 0 1 6.5v8a.5.5 0 0 0 1 0v-8A1.5 1.5 0 0 1 3.5 5h9.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4z"/>
|
|
</svg>
|
|
<div class="copy-hint-text hidden">Your Signature is Downloaded!<br>Click to Copy to Clipboard</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="sig-ver-url-container" class="sig-ver-url-container hidden">
|
|
<label for="sig-ver-url">QR Code URL:</label>
|
|
<input type="text" id="sig-ver-url" class="sig-ver-url" readonly>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<input type="text" id="name-input" placeholder="Enter signature name" />
|
|
<div class="font-select-container">
|
|
<div id="font-select-display" class="font-select-display">Select Font</div>
|
|
<div id="font-options" class="font-options hidden"></div>
|
|
</div>
|
|
<input type="checkbox" id="invert-toggle">
|
|
<label for="invert-toggle">Invert</label>
|
|
<button id="go-button" class="go-button">Go!</button>
|
|
</div>
|
|
|
|
<div class="button-container">
|
|
<a href="/howto" class="button">How To Use OSG</a>
|
|
<a href="/privacy" class="button">Privacy Policy</a>
|
|
<a href="https://code.karsttech.com/jeremy/open-signature-generator" class="button">Source Code</a>
|
|
</div>
|
|
|
|
<div id="animated-background">
|
|
<svg viewBox="0 0 100 100"></svg>
|
|
</div>
|
|
</body>
|
|
|
|
<script>
|
|
const nameInput = document.getElementById('name-input');
|
|
const fontSelectDisplay = document.getElementById('font-select-display');
|
|
const signatureImage = document.querySelector('.signature-image');
|
|
const signatureImageInverted = document.querySelector('.signature-image-inverted');
|
|
const invertToggle = document.getElementById('invert-toggle');
|
|
const goButton = document.getElementById('go-button');
|
|
const documentContainer = document.getElementById('document-container');
|
|
const documentContent = document.querySelector('.document-content');
|
|
const signatureLine = document.getElementById('signature-line');
|
|
const animatedBackground = document.getElementById('animated-background');
|
|
const sigVerUrlContainer = document.getElementById('sig-ver-url-container');
|
|
|
|
function nameInputCallback() {
|
|
// Save signature name to local storage
|
|
localStorage.setItem('signatureName', nameInput.value);
|
|
}
|
|
nameInput.addEventListener('change', nameInputCallback);
|
|
|
|
function invertSignature() {
|
|
if (invertToggle.checked) {
|
|
signatureImage.classList.add('hidden');
|
|
signatureImageInverted.classList.remove('hidden');
|
|
documentContainer.style.setProperty("background-color", "rgb(20, 20, 20)");
|
|
documentContent.style.setProperty("color", "white");
|
|
signatureLine.style.setProperty("border-color", "white");
|
|
} else {
|
|
signatureImage.classList.remove('hidden');
|
|
signatureImageInverted.classList.add('hidden');
|
|
documentContainer.style.setProperty("background-color", "rgb(236, 236, 234)");
|
|
documentContent.style.setProperty("color", "black");
|
|
signatureLine.style.setProperty("border-color", "black");
|
|
}
|
|
}
|
|
|
|
function copyImageToClipboard(image) {
|
|
fetch(image.src).then(response => {
|
|
response.blob().then(blob => {
|
|
navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
});
|
|
});
|
|
}
|
|
signatureImage.addEventListener('click', () => {
|
|
copyImageToClipboard(signatureImage);
|
|
});
|
|
signatureImageInverted.addEventListener('click', () => {
|
|
copyImageToClipboard(signatureImageInverted);
|
|
});
|
|
|
|
invertToggle.addEventListener('change', invertSignature);
|
|
goButton.addEventListener('click', () => {
|
|
var name = nameInput.value.trim(); // Trim whitespace from name
|
|
|
|
// Add validation for empty name
|
|
if (!name) {
|
|
nameInput.classList.add('error-shake');
|
|
setTimeout(() => nameInput.classList.remove('error-shake'), 600);
|
|
return;
|
|
}
|
|
|
|
var font = fontSelectDisplay.innerHTML;
|
|
var invert = invertToggle.checked;
|
|
var timezone = -1 * new Date().getTimezoneOffset();
|
|
|
|
// Hide the copy hint elements if they are not already hidden
|
|
document.querySelector('.copy-hint-arrow').classList.add('hidden');
|
|
document.querySelector('.copy-hint-text').classList.add('hidden');
|
|
|
|
// Clear the sig-ver-url container
|
|
sigVerUrlContainer.value = '';
|
|
sigVerUrlContainer.classList.add('hidden');
|
|
|
|
// Disable the go button
|
|
goButton.disabled = true;
|
|
|
|
// Show a progress spinner
|
|
goButton.innerHTML = 'Generating...';
|
|
goButton.classList.add('go-spinner');
|
|
|
|
fetch(`register?name=${encodeURIComponent(name)}&font=${encodeURIComponent(font)}&timezone=${encodeURIComponent(timezone)}&invert=${invert}`, {
|
|
method: 'GET',
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
let fetch_data = response.text();
|
|
fetch_data.then(image_id => {
|
|
let image_url = `/images/${image_id}.png`;
|
|
var image_element;
|
|
if (!invertToggle.checked) {
|
|
image_element = signatureImage;
|
|
} else {
|
|
image_element = signatureImageInverted;
|
|
}
|
|
image_element.src = image_url;
|
|
|
|
// Display the image ID
|
|
const urlInput = document.getElementById('sig-ver-url');
|
|
let sig_ver_url = `${window.location.origin}/v/${image_id}`;
|
|
urlInput.value = sig_ver_url;
|
|
sigVerUrlContainer.classList.remove('hidden');
|
|
|
|
// Add click-to-copy functionality
|
|
urlInput.addEventListener('click', function() {
|
|
this.select();
|
|
navigator.clipboard.writeText(this.value);
|
|
});
|
|
|
|
// Wait until the image is loaded
|
|
image_element.onload = () => {
|
|
// Show the copy hint elements
|
|
document.querySelector('.copy-hint-arrow').classList.remove('hidden');
|
|
document.querySelector('.copy-hint-text').classList.remove('hidden');
|
|
|
|
// Save image to disk
|
|
let a = document.createElement('a');
|
|
a.href = image_url;
|
|
a.download = `Signature-${name}.png`;
|
|
a.click();
|
|
|
|
// Re-enable the go button
|
|
goButton.disabled = false;
|
|
goButton.innerHTML = 'Go!';
|
|
goButton.classList.remove('go-spinner');
|
|
};
|
|
});
|
|
} else {
|
|
if (response.status === 429) {
|
|
// Notify the user that they are being rate limited
|
|
alert('Too many requests, try again later.');
|
|
}
|
|
// Re-enable the go button
|
|
goButton.disabled = false;
|
|
goButton.innerHTML = 'Go!';
|
|
goButton.classList.remove('go-spinner');
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
});
|
|
});
|
|
|
|
// Add this function to load and parse fonts from server
|
|
async function loadFonts() {
|
|
const response = await fetch('/fonts');
|
|
const fontList = await response.json();
|
|
const fonts = Object.keys(fontList);
|
|
|
|
const fontOptionsContainer = document.getElementById('font-options');
|
|
const fontDisplay = document.getElementById('font-select-display');
|
|
fontOptionsContainer.innerHTML = ''; // Clear existing options
|
|
|
|
// Load each font and create styled divs
|
|
const fontLoadPromises = fonts.map(async (fontName) => {
|
|
const fontPath = `/fonts/${fontList[fontName]}`;
|
|
const font = new FontFace(fontName, `url(${fontPath})`);
|
|
|
|
try {
|
|
await font.load();
|
|
document.fonts.add(font);
|
|
|
|
// Create and style option div
|
|
const option = document.createElement('div');
|
|
option.className = 'font-option';
|
|
option.dataset.value = fontName;
|
|
option.textContent = fontName;
|
|
option.style.fontFamily = `"${font.family}"`;
|
|
|
|
option.addEventListener('click', () => {
|
|
fontDisplay.textContent = fontName;
|
|
fontDisplay.style.fontFamily = `"${font.family}"`;
|
|
fontOptionsContainer.classList.add('hidden');
|
|
localStorage.setItem('selectedFont', fontName);
|
|
});
|
|
|
|
option.addEventListener('mouseenter', () => {
|
|
fontDisplay.textContent = fontName;
|
|
fontDisplay.style.fontFamily = `"${font.family}"`;
|
|
});
|
|
|
|
fontOptionsContainer.appendChild(option);
|
|
} catch (err) {
|
|
console.error(`Failed to load font ${fontName}:`, err);
|
|
}
|
|
});
|
|
|
|
await Promise.all(fontLoadPromises);
|
|
|
|
// Setup display click handler
|
|
fontDisplay.addEventListener('click', () => {
|
|
fontOptionsContainer.classList.remove('hidden');
|
|
});
|
|
|
|
// Close options when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.font-select-container')) {
|
|
fontOptionsContainer.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// Set initial font
|
|
if (localStorage.getItem('selectedFont')) {
|
|
const savedFont = localStorage.getItem('selectedFont');
|
|
const option = fontOptionsContainer.querySelector(`[data-value="${savedFont}"]`);
|
|
if (option) {
|
|
option.click();
|
|
}
|
|
} else {
|
|
// Choose a random font
|
|
const options = fontOptionsContainer.querySelectorAll('.font-option');
|
|
const randomOption = options[Math.floor(Math.random() * options.length)];
|
|
if (randomOption) {
|
|
randomOption.click();
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopAnimations() {
|
|
const animations = animatedBackground.querySelectorAll('animate, animateTransform');
|
|
animations.forEach(animation => {
|
|
animation.style.setProperty('animation-play-state', 'paused');
|
|
});
|
|
}
|
|
|
|
sigVerUrlContainer.addEventListener('click', () => {
|
|
sigVerUrlContainer.select();
|
|
});
|
|
|
|
// Modify your initialization code
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Fetch and populate fonts
|
|
loadFonts();
|
|
// Load signature name and font from local storage
|
|
nameInput.value = localStorage.getItem('signatureName') || '';
|
|
// Stop animations after 5 minutes to save resources
|
|
setTimeout(stopAnimations, 300 * 1000);
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
body {
|
|
background-color: #000000;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
min-height: 100vh;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
|
|
#title-container {
|
|
position: absolute;
|
|
width: 100%;
|
|
top: 20px;
|
|
z-index: 2;
|
|
text-align: center;
|
|
}
|
|
|
|
#title-container h1 {
|
|
font-size: clamp(24px, calc(6vw + 8px), 64px);
|
|
margin-top: 20px;
|
|
color: white;
|
|
text-shadow: 5px 5px 10px rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
#document-container {
|
|
background-color: rgb(236, 236, 234);
|
|
width: 70%;
|
|
max-width: 500px;
|
|
height: 300px;
|
|
margin: 100px auto 20px auto;
|
|
padding: 40px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
position: relative;
|
|
z-index: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.document-content {
|
|
position: absolute;
|
|
width: calc(100% - 80px);
|
|
bottom: 100px;
|
|
filter: blur(4px);
|
|
color: #181818;
|
|
line-height: 1.5;
|
|
font-size: 1em;
|
|
}
|
|
|
|
.signature-area{
|
|
position: absolute;
|
|
bottom: 0;
|
|
width: calc(100% - 80px);
|
|
height: 80px;
|
|
padding-top: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.document-content p {
|
|
text-indent: 4em;
|
|
}
|
|
|
|
.signature-image {
|
|
position: absolute;
|
|
filter: none;
|
|
text-align: left;
|
|
bottom: 0;
|
|
transform: translateY(20%);
|
|
width: 60%;
|
|
height: auto;
|
|
max-width: 500px;
|
|
}
|
|
|
|
/* .signature-image-inverted {
|
|
} */
|
|
|
|
#signature-line {
|
|
position: absolute;
|
|
border-bottom: 2px solid black;
|
|
width: 60%;
|
|
bottom: 0;
|
|
}
|
|
|
|
.controls {
|
|
position: relative;
|
|
bottom: auto;
|
|
margin: 20px auto;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
z-index: 2;
|
|
width: 80%;
|
|
max-width: 540px;
|
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.controls input[type='text'] {
|
|
width: 30%;
|
|
margin: auto 0;
|
|
}
|
|
|
|
.controls select {
|
|
width: 30%;
|
|
margin: auto 0;
|
|
}
|
|
|
|
.controls input[type='checkbox'] {
|
|
width: 15px;
|
|
margin: auto 0;
|
|
}
|
|
|
|
.controls label {
|
|
width: 5%;
|
|
font-size: 0.8em;
|
|
position: relative;
|
|
left: -12px;
|
|
margin: auto 0;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.controls button {
|
|
position: relative;
|
|
width: 20%;
|
|
margin: auto 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
input, select {
|
|
padding: 6px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
}
|
|
|
|
.go-button {
|
|
margin: 10px;
|
|
font-weight: bold;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.go-spinner {
|
|
padding: 2px;
|
|
border: 2px solid #ccc;
|
|
background: linear-gradient(90deg, #010030, #ac0000, #000000);
|
|
background-size: 200% 100%;
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
animation: gradient-sweep 2s linear infinite;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
@keyframes gradient-sweep {
|
|
0% { background-position: 200% 50%; }
|
|
100% { background-position: -100% 50%; }
|
|
}
|
|
|
|
.copy-hint-arrow {
|
|
position: absolute;
|
|
right: 40px;
|
|
top: 0px;
|
|
transform: scale(-0.5, -0.5);
|
|
pointer-events: none;
|
|
animation: fadeIn 2s ease-in ;
|
|
width: 30%;
|
|
height: 150%;
|
|
}
|
|
|
|
@keyframes animateStroke {
|
|
0% { stroke-dashoffset: 200;
|
|
opacity: 0; }
|
|
100% { stroke-dashoffset: 0;
|
|
opacity: 1; }
|
|
}
|
|
|
|
.copy-hint-arrow path {
|
|
stroke-dasharray: 200;
|
|
stroke: rgba(0,0,0,0.9);
|
|
stroke-width: 2;
|
|
fill: none;
|
|
animation: animateStroke 2s ease-in;
|
|
}
|
|
|
|
.copy-hint-arrow polygon {
|
|
fill: rgba(0,0,0,0.9);
|
|
}
|
|
|
|
.copy-hint-text {
|
|
position: absolute;
|
|
font-weight: bold;
|
|
right: 25px;
|
|
bottom: 85px;
|
|
color: rgba(0,0,0,0.9);
|
|
font-size: 18px;
|
|
animation: fadeIn 1s ease-in;
|
|
pointer-events: none;
|
|
text-align: center;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
0% { opacity: 0; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
#font-select {
|
|
padding: 8px;
|
|
font-size: 18px;
|
|
text-align: center;
|
|
}
|
|
|
|
#animated-background {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: -1;
|
|
overflow: clip;
|
|
}
|
|
|
|
@property --a {
|
|
syntax: '<color>';
|
|
inherits: false;
|
|
initial-value: #453f53;
|
|
}
|
|
@property --b {
|
|
syntax: '<color>';
|
|
inherits: false;
|
|
initial-value: #fff;
|
|
}
|
|
@property --c {
|
|
syntax: '<color>';
|
|
inherits: false;
|
|
initial-value: #777;
|
|
}
|
|
|
|
#animated-background svg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--pattern), var(--map, linear-gradient(90deg, #888, #fff));
|
|
background-blend-mode: multiply;
|
|
filter: contrast(5) blur(20px) saturate(35%) brightness(0.4);
|
|
mix-blend-mode: darken;
|
|
--pattern: repeating-radial-gradient(circle, var(--a), var(--b), var(--c) 15em);
|
|
/* We use steps here to limit framerate to reduce CPU usage displaying the animation*/
|
|
/* Because it is a slowly changing background, a low framerate does not impact apparent smoothness of the animation */
|
|
animation: bganimation 120s forwards steps(1200) infinite;
|
|
transform: translateX(35%) translateY(75%) scale(4.5)
|
|
}
|
|
|
|
@keyframes bganimation {
|
|
0% { --a: #453f53;
|
|
--b: #fff;
|
|
--c: #777;
|
|
transform: translateX(35%) translateY(75%) scale(4.5)}
|
|
33% { --a: #ce8083;
|
|
--b: #ac8cbd;
|
|
--c: #3b1c80;
|
|
transform: rotate(-10deg) scale(4.0,3.5) translateX(15%) translateY(25%)}
|
|
66% { --a: #309385;
|
|
--b: #5aa8fb;
|
|
--c: #866849;
|
|
transform: rotate(10deg) scale(4.5,3.5) translateX(25%) translateY(-15%)}
|
|
100% { --a: #453f53;
|
|
--b: #fff;
|
|
--c: #777;
|
|
transform: translateX(35%) translateY(75%) scale(4.5)}
|
|
}
|
|
|
|
.font-select-container {
|
|
position: relative;
|
|
width: 30%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.font-select-display {
|
|
padding: 6px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
background: white;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
font-size: 2vh;
|
|
height: 25px;
|
|
user-select: none;
|
|
overflow: clip;
|
|
}
|
|
|
|
.font-options {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: -20px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: white;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
z-index: 1000;
|
|
box-shadow: 0 -2px 4px rgba(0,0,0,0.2);
|
|
font-size: 3vh;
|
|
width: calc(100% + 40px);
|
|
}
|
|
|
|
.font-option {
|
|
padding: 8px;
|
|
cursor: pointer;
|
|
text-align: center;
|
|
user-select: none;
|
|
position: relative;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.font-option:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.sig-ver-url-container {
|
|
left: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 8px;
|
|
gap: 8px;
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
background: #fff;
|
|
}
|
|
|
|
.sig-ver-url {
|
|
flex: 1;
|
|
color: #333;
|
|
cursor: pointer;
|
|
min-width: 250px;
|
|
font-size: 0.7em;
|
|
}
|
|
|
|
.sig-ver-url:hover {
|
|
background: #ccd0eb;
|
|
user-select: all;
|
|
}
|
|
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-5px); }
|
|
75% { transform: translateX(5px); }
|
|
}
|
|
|
|
.error-shake {
|
|
animation: shake 0.2s ease-in-out 0s 3;
|
|
background-color: #ffebee;
|
|
border-color: #ff5252;
|
|
}
|
|
|
|
.button-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.button {
|
|
padding: 10px 20px;
|
|
background-color: #1f4e82;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
.button:hover {
|
|
background-color: #095fbf;
|
|
}
|
|
|
|
</style> |