open-signature-generator/web_content/index.html

739 lines
23 KiB
HTML
Raw Normal View History

2025-01-14 23:30:21 -05:00
<!DOCTYPE html>
<html lang="en">
2025-01-14 13:17:55 -05:00
<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" />
2025-01-14 13:17:55 -05:00
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
2025-01-14 23:30:21 -05:00
<!-- Add critical layout CSS inline to prevent layout shift -->
<style>
.hidden {
display: none !important;
}
body {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
}
#document-container {
width: 70%;
max-width: 500px;
height: 300px;
margin: 100px auto 20px auto;
padding: 40px;
}
.document-content {
position: absolute;
width: calc(100% - 80px);
bottom: 100px;
line-height: 1.5;
font-size: 1em;
}
.controls {
margin: 20px auto;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 20px;
width: 80%;
max-width: 540px;
}
#title-container {
position: absolute;
width: 100%;
top: 20px;
text-align: center;
}
</style>
2025-01-14 13:17:55 -05:00
</head>
2025-01-14 23:30:21 -05:00
2025-01-14 13:17:55 -05:00
<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">
2025-01-14 22:06:25 -05:00
<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" />
2025-01-14 13:17:55 -05:00
<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>
2025-01-14 16:03:45 -05:00
<a href="https://code.karsttech.com/jeremy/open-signature-generator" class="button">Source Code</a>
2025-01-14 13:17:55 -05:00
</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;
}
2025-01-14 23:30:21 -05:00
</style>
</html>