Forum Discussion
Digital Signature Submission to Complete
Rise Course
https://360.articulate.com/review/content/26a0e895-1ebf-4fb5-9835-fd327aa5f03b/review
Inspiration
Our organization sometimes needs policy acknowledgements from employees. Previously, we used a the Rise list block with a single checkbox for users to indicate understanding; clicking it enabled the continue button, which let users complete the course. This marked the course as complete or incomplete in our LMS, giving us a report of who had made the policy acknowledgement
Now, I aim to create a more meaningful experience by having learners sign and submit their names before proceeding, rather than just checking a box. Although the functionality is similar - interaction enables the continue button and tracks completion - the signature process adds authenticity compared to simply checking a box.
Tools
- Copilot - vibe coded HTML, CSS, Javascript to build fully functional code block
- Visual Studio Code (VS Code) – code editor used to easily find parts of the code to tweak and adjust (ex. verbiage, fonts, colors, etc) since asking Copilot to change these things often ended up in functionality-breaking changes.
What I Learned
This approach was faster, easier, and more scalable than recreating the activity in Storyline or coding from scratch. My company limits the use of AI tools, so Copilot was my only tool for vibe coding. I also had some issues getting the “Set completion requirements” functionality to work, but was able to prompt Copilot to get success (prompting is pasted below)! While it worked in the end, toying with other tools on my personal computer elicits better, faster coding responses. But hey, use the tools you have access to!
Prompts Used
Initial Prompt:
"Keeping all coding, CSS, and JavaScript inside of one HTML page, create a page where the user uses the mouse to write a signature. Create two buttons below the signature box. One should be a reset button that will erase any writing in the signature box, resetting any inputs there. When clicking the reset signature button, there should be a quick erase animation to clear the box. The other button should start as grey, but once the user clicks into the signature box to enter their signature it becomes active and states Submit. After clicking submit, there should be an animation indicating the signature has been submitted.”
Additional Prompts required to get the UI to fully functional:
- “Right now things mostly work, but after clicking submit on the signature if the mouse hovers over the drawing section it resets the signature and unsubmits. can you adjust the code so that the signature only resets if the user clicks the Reset Signature button?”
- “okay now it resets when you click into the signature still. can you update the code so the signature only resets with clicking reset signature button?”
- “actually, update the code so that when a user clicks submit the submit animation displays and stays indefinitely unless the user clicks rest signature”
- “okay yes that works, but a couple more tweaks. Please require ink before submission. Second, is there a way to ONLY make postMessage({ type: 'complete' }, '*') fire when the signature is submitted? So if they click ‘resest signature,’ it goes back to being ' incomplete ' , then can be made as 'complete' if they resubmit the signature”
- “Rise course doesn’t respond to 'incomplete', keep the UI logic as-is and only send 'complete' on submit.”
The Final Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Signature Capture</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Fonts: Lato (headers) + Merriweather (body) -->
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@700;900&family=Merriweather:wght@300;400;700&display=swap" rel="stylesheet">
<style>
:root{
/* Colors */
--border: #d9dce3;
--border-strong: #b7bcc7;
--ink: #111318;
--accent: #2563eb; /* blue */
--success: #22c55e; /* green */
--disabled: #9aa2af;
--btn-text: #0f172a;
--white: #fff;
}
html, body {
margin: 0;
padding: 0;
/* Transparent background to blend with Rise */
background: transparent;
/* Body font: Merriweather */
font-family: "Merriweather", Georgia, serif;
color: #0f172a;
}
.page {
max-width: 720px;
margin: 40px auto;
padding: 24px;
/* Transparent wrapper to avoid visible boxes behind Rise theme */
background: transparent;
}
h1, h2, h3, h4 {
/* Headings font: Lato */
font-family: "Lato", "Segoe UI", Roboto, Arial, sans-serif;
font-weight: 700;
}
h1 {
font-size: 1.25rem;
margin: 0 0 12px 0;
}
.desc {
color: #475569;
margin-bottom: 16px;
font-size: 0.95rem;
}
/* Signature box */
.sig-wrap {
position: relative;
background: var(--white);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.06);
overflow: hidden;
transition: box-shadow 220ms ease;
}
.sig-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-bottom: 1px dashed var(--border);
font-size: 0.9rem;
color: #475569;
/* Header text uses Lato for visual consistency */
font-family: "Lato", "Segoe UI", Roboto, Arial, sans-serif;
}
.sig-canvas {
display: block;
width: 100%;
height: 240px; /* visual CSS height; drawing code makes it crisp */
cursor: crosshair;
background: #fff;
}
.sig-wrap.locked .sig-canvas {
cursor: not-allowed; /* visual cue when locked after submit */
}
.sig-base-line {
position: absolute;
left: 12px;
right: 12px;
bottom: 16px;
border-bottom: 1px dashed #cbd5e1;
pointer-events: none;
}
/* Helper placeholder text */
.placeholder {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 0.95rem;
color: #94a3b8;
pointer-events: none;
user-select: none;
transition: opacity 160ms ease;
font-family: "Lato", "Segoe UI", Roboto, Arial, sans-serif;
}
.placeholder.hidden { opacity: 0; }
/* Submit success overlay (persistent until Reset) */
.submit-overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.65);
opacity: 0;
pointer-events: none; /* overlay is visual only */
transition: opacity 140ms ease;
}
.submit-overlay.show { opacity: 1; }
.checkwrap {
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--success);
display: grid;
place-items: center;
box-shadow: 0 10px 25px rgba(34, 197, 94, 0.35);
animation: pop 380ms ease-out;
}
@keyframes pop {
0% { transform: scale(0.6); }
60% { transform: scale(1.08); }
100% { transform: scale(1); }
}
.checkwrap svg {
width: 48px;
height: 48px;
stroke: #fff;
stroke-width: 6;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
/* Draw animation */
stroke-dasharray: 120;
stroke-dashoffset: 120;
animation: drawcheck 500ms 180ms ease-out forwards;
}
@keyframes drawcheck {
to { stroke-dashoffset: 0; }
}
/* Persistent submitted glow while overlay is shown */
.sig-wrap.submitted {
box-shadow: 0 0 0 2px rgba(34,197,94,0.35), 0 6px 22px rgba(34,197,94,0.25);
}
/* Button row */
.controls {
display: flex;
gap: 12px;
margin-top: 14px;
}
.btn {
appearance: none;
border: 1px solid var(--border);
background: #eef2f7;
color: var(--btn-text);
border-radius: 10px;
padding: 10px 16px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: background 150ms ease, color 150ms ease, box-shadow 150ms ease, border-color 150ms ease, transform 100ms ease;
font-family: "Lato", "Segoe UI", Roboto, Arial, sans-serif;
}
.btn:hover { transform: translateY(-0.5px); }
.btn:active { transform: translateY(0); }
.btn.reset {
background: #f1f5f9;
}
.btn.reset:hover {
background: #e2e8f0;
}
.btn.submit {
background: #f1f5f9;
color: #64748b;
border-color: var(--border);
}
.btn.submit:disabled {
cursor: not-allowed;
color: var(--disabled);
background: #e9edf3;
border-color: #d3d8e2;
}
.btn.submit.enabled {
background: var(--accent);
color: #fff;
border-color: #1e54cb;
box-shadow: 0 6px 18px rgba(37,99,235,0.35);
}
.btn.submit.enabled:hover {
background: #1e54cb;
}
/* Live region for status messages (visually hidden) */
.sr-only {
position: absolute !important;
height: 1px; width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
border: 0;
padding: 0; margin: 0;
}
</style>
</head>
<body>
<main class="page">
<h1>To acknoweldge the policy, sign your name and click submit below.</h1>
<p class="desc">Use your mouse to write your signature in the box, then Submit. To erase and try again, click Reset.</p>
<section class="sig-wrap" id="sigWrap" aria-label="Signature capture area">
<div class="sig-header">
<span>Click and drag to sign</span>
<span>Mouse only</span>
</div>
<canvas id="sigCanvas" class="sig-canvas" aria-label="Signature canvas"></canvas>
<div class="placeholder" id="placeholder">Click and drag to sign</div>
<div class="sig-base-line"></div>
<!-- Submit success overlay (persistent until reset) -->
<div class="submit-overlay" id="submitOverlay" aria-hidden="true">
<div class="checkwrap" role="img" aria-label="Signature submitted">
<svg viewBox="0 0 64 64" aria-hidden="true">
<path d="M16 34 L28 46 L48 20"></path>
</svg>
</div>
</div>
<div class="sr-only" aria-live="polite" id="liveStatus"></div>
</section>
<div class="controls">
<button type="button" class="btn reset" id="resetBtn">Reset</button>
<button type="button" class="btn submit" id="submitBtn" disabled>Submit</button>
</div>
</main>
<script>
(function() {
const wrap = document.getElementById('sigWrap');
const canvas = document.getElementById('sigCanvas');
const ctx = canvas.getContext('2d');
const placeholder= document.getElementById('placeholder');
const resetBtn = document.getElementById('resetBtn');
const submitBtn = document.getElementById('submitBtn');
const overlay = document.getElementById('submitOverlay');
const liveStatus = document.getElementById('liveStatus');
let isDrawing = false;
let hasSignature = false;
let isSubmitted = false; // lock flag
// Report completion only once per submit (until reset)
let completionReported = false;
/* ---- Pen style ---- */
function setPenStyle() {
ctx.lineWidth = 2.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--ink') || '#111318';
}
/* ---- Buttons & status ---- */
function enableSubmit() {
submitBtn.disabled = false;
submitBtn.classList.add('enabled');
}
function disableSubmit() {
submitBtn.disabled = true;
submitBtn.classList.remove('enabled');
}
function announce(msg) {
liveStatus.textContent = msg;
}
/* ---- Rise 360 messaging ---- */
function reportCompletion() {
if (completionReported) return;
try {
window.parent.postMessage({ type: 'complete' }, '*');
completionReported = true;
console.log('Rise 360 completion posted.');
} catch (err) {
console.warn('Unable to post completion to parent window:', err);
}
}
/* ---- Lock/unlock after submit/reset ---- */
function lockSignature() {
isSubmitted = true;
canvas.style.pointerEvents = 'none';
wrap.classList.add('locked');
overlay.classList.add('show'); // keep overlay visible indefinitely
wrap.classList.add('submitted'); // persistent glow while overlay is shown
// Disable Submit while locked to avoid duplicate submissions
submitBtn.disabled = true;
submitBtn.classList.remove('enabled');
// Notify Rise 360 that this block is complete
reportCompletion();
}
function unlockSignature() {
isSubmitted = false;
canvas.style.pointerEvents = 'auto';
wrap.classList.remove('locked');
overlay.classList.remove('show'); // hide overlay on reset
wrap.classList.remove('submitted');
// Do NOT post 'incomplete' (per your setup)
completionReported = false; // allows re-reporting on next submit
}
/* ---- Geometry ---- */
function getPos(e) {
const rect = canvas.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
/* ---- Resize (preserve strokes) ---- */
function resizeCanvas({ preserve = true } = {}) {
const rect = canvas.getBoundingClientRect();
let dataURL = null;
if (preserve) {
try { dataURL = canvas.toDataURL('image/png'); } catch (_) {}
}
const ratio = Math.max(1, Math.floor(window.devicePixelRatio || 1));
canvas.width = Math.floor(rect.width * ratio);
canvas.height = Math.floor(rect.height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
ctx.clearRect(0, 0, rect.width, rect.height);
setPenStyle();
if (dataURL) {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, rect.width, rect.height);
hasSignature = true;
placeholder.classList.add('hidden');
if (!isSubmitted) enableSubmit();
};
img.src = dataURL;
} else {
hasSignature = false;
placeholder.classList.remove('hidden');
disableSubmit();
}
}
/* ---- Drawing (blocked when submitted) ---- */
canvas.addEventListener('mousedown', (e) => {
if (isSubmitted) return; // no drawing while locked
const { x, y } = getPos(e);
isDrawing = true;
ctx.beginPath();
ctx.moveTo(x, y);
placeholder.classList.add('hidden');
// IMPORTANT: Require ink before enabling submit (do not enable here)
});
window.addEventListener('mousemove', (e) => {
if (!isDrawing || isSubmitted) return;
const rect = canvas.getBoundingClientRect();
if (e.clientX < rect.left || e.clientX > rect.right ||
e.clientY < rect.top || e.clientY > rect.bottom) {
return;
}
const { x, y } = getPos(e);
ctx.lineTo(x, y);
ctx.stroke();
// Once actual ink is drawn, enable submit
if (!hasSignature) {
hasSignature = true;
if (!isSubmitted) enableSubmit();
}
});
window.addEventListener('mouseup', () => {
if (!isDrawing) return;
isDrawing = false;
});
canvas.addEventListener('mouseleave', () => {
if (!isDrawing) return;
isDrawing = false;
});
/* ---- Clear ONLY via Reset ---- */
async function eraseWithAnimation() {
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const duration = 300;
const start = performance.now();
const wipe = document.createElement('div');
wipe.style.position = 'absolute';
wipe.style.top = (canvas.offsetTop) + 'px';
wipe.style.left = (canvas.offsetLeft) + 'px';
wipe.style.width = width + 'px';
wipe.style.height = height + 'px';
wipe.style.pointerEvents = 'none';
wipe.style.background = 'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 50%, rgba(255,255,255,0) 100%)';
wipe.style.opacity = '0.0';
wipe.style.transition = 'opacity 120ms ease';
wrap.appendChild(wipe);
requestAnimationFrame(() => { wipe.style.opacity = '1.0'; });
return new Promise(resolve => {
function step(now) {
const t = Math.min(1, (now - start) / duration);
const wipeX = width * t;
ctx.clearRect(0, 0, wipeX, height);
if (t < 1) {
requestAnimationFrame(step);
} else {
ctx.clearRect(0, 0, width, height);
hasSignature = false;
placeholder.classList.remove('hidden');
announce('Signature cleared.');
wipe.remove();
unlockSignature(); // hide overlay and allow drawing again
disableSubmit(); // user must draw again before submitting
resolve();
}
}
requestAnimationFrame(step);
});
}
/* ---- Submit (requires ink, persistent overlay + Rise completion) ---- */
function playSubmitAnimation() {
announce('Signature submitted.');
try {
const dataURL = canvas.toDataURL('image/png');
console.log('Submitted signature (PNG data URL):', dataURL);
} catch (err) {
console.warn('Unable to produce data URL:', err);
}
}
resetBtn.addEventListener('click', async () => {
isDrawing = false;
await eraseWithAnimation();
});
submitBtn.addEventListener('click', () => {
if (submitBtn.disabled) return;
// Require ink before submission
if (!hasSignature) {
announce('Please sign before submitting.');
return;
}
playSubmitAnimation();
lockSignature(); // keep overlay/glow indefinitely until Reset (and trigger completion)
});
/* ---- Init ---- */
window.addEventListener('resize', () => resizeCanvas({ preserve: true }));
resizeCanvas({ preserve: false }); // initial setup
setPenStyle();
})();
</script>
</body>
</html>
1 Reply
- Rach_UnicornCommunity Member
What a great idea!
Related Content
- 4 months ago
- 5 months ago