Pathway to a Gift Generator
:root{
–bg:#0b1220;
–card:#111a2e;
–muted:#a9b4d0;
–text:#e9eeff;
–accent:#7aa2ff;
–border:#26304a;
}
body{
margin:0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(180deg, #070b14, #0b1220 40%, #070b14);
color:var(–text);
}
.wrap{
max-width:1100px;
margin:24px auto 60px;
padding:0 16px;
}
h1{
margin:0 0 6px;
font-size:26px;
letter-spacing:.2px;
}
.sub{
color:var(–muted);
margin:0 0 18px;
line-height:1.35;
}
.grid{
display:grid;
grid-template-columns: 1fr 1fr;
gap:16px;
align-items:start;
}
@media (max-width: 980px){
.grid{ grid-template-columns:1fr; }
}
.card{
background: rgba(17,26,46,.92);
border:1px solid var(–border);
border-radius:14px;
padding:16px;
box-shadow: 0 12px 35px rgba(0,0,0,.35);
}
label{
display:block;
font-weight:650;
margin:10px 0 6px;
font-size:13px;
color:#dbe3ff;
}
input[type=”text”], textarea{
width:100%;
box-sizing:border-box;
border-radius:10px;
border:1px solid var(–border);
background:#0c1427;
color:var(–text);
padding:10px 12px;
outline:none;
}
textarea{
min-height:70px;
resize:vertical;
line-height:1.25;
}
input[type=”text”]:focus, textarea:focus{
border-color: rgba(122,162,255,.75);
box-shadow: 0 0 0 3px rgba(122,162,255,.12);
}
.row{
display:grid;
grid-template-columns: 160px 1fr;
gap:12px;
align-items:end;
}
@media (max-width: 620px){
.row{ grid-template-columns: 1fr; }
}
.eyebrow{
font-size:12px;
color:var(–muted);
margin-top:-2px;
}
.count{
font-size:12px;
color:var(–muted);
float:right;
margin-top:-22px;
}
.btns{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:14px;
}
button{
border:0;
border-radius:12px;
padding:10px 14px;
font-weight:700;
cursor:pointer;
color:#071026;
background: linear-gradient(180deg, #9fb8ff, #7aa2ff);
}
button.secondary{
background: transparent;
color:var(–text);
border:1px solid var(–border);
}
button:active{ transform: translateY(1px); }
.preview{
display:flex;
flex-direction:column;
gap:10px;
}
.canvasWrap{
background:#ffffff;
border-radius:14px;
overflow:hidden;
border:1px solid var(–border);
}
.hint{
color:var(–muted);
font-size:13px;
line-height:1.35;
margin:0;
}
a.download{
display:inline-block;
color:#cfe0ff;
text-decoration:none;
border:1px solid var(–border);
padding:8px 10px;
border-radius:12px;
width:max-content;
}
a.download:hover{
border-color: rgba(122,162,255,.75);
}
.tiny{
font-size:12px;
color:var(–muted);
margin:6px 0 0;
}
Pathway to a Gift Generator
Enter up to 8 engagements (180 characters each). Click Generate Image to produce a PNG you can download.
Prospect Name
Engagements (Month/Year sits above the engagement text)
Tip: Use Month/Year like “JAN 2023” (or “01/2023”) — the generator prints what you type.
Preview (generated client-side). You can right-click to save, or use the download button.
// ———- Form UI generation ———-
const engagementFieldsEl = document.getElementById(“engagementFields”);
const MAX_ENGAGEMENTS = 8;
const MAX_CHARS = 180;
function makeEngagementBlock(i){
const n = i + 1;
const block = document.createElement(“div”);
block.style.marginTop = “14px”;
block.style.padding = “12px”;
block.style.border = “1px solid var(–border)”;
block.style.borderRadius = “12px”;
block.style.background = “#0a1020”;
const title = document.createElement(“div”);
title.style.fontWeight = “800”;
title.style.marginBottom = “6px”;
title.textContent = `Engagement ${n}`;
block.appendChild(title);
// Month/Year ABOVE engagement text field, but still “paired”
const monthLabel = document.createElement(“label”);
monthLabel.setAttribute(“for”, `m${n}`);
monthLabel.textContent = “Month/Year”;
block.appendChild(monthLabel);
const monthInput = document.createElement(“input”);
monthInput.type = “text”;
monthInput.id = `m${n}`;
monthInput.name = `m${n}`;
monthInput.placeholder = “e.g., JAN 2025”;
monthInput.maxLength = 12;
block.appendChild(monthInput);
const engLabel = document.createElement(“label”);
engLabel.setAttribute(“for”, `e${n}`);
engLabel.style.marginTop = “10px”;
engLabel.textContent = `Engagement ${n} (max ${MAX_CHARS} chars)`;
block.appendChild(engLabel);
const counter = document.createElement(“div”);
counter.className = “count”;
counter.id = `c${n}`;
counter.textContent = `0/${MAX_CHARS}`;
block.appendChild(counter);
const ta = document.createElement(“textarea”);
ta.id = `e${n}`;
ta.name = `e${n}`;
ta.maxLength = MAX_CHARS;
ta.placeholder = “Type engagement details…”;
ta.addEventListener(“input”, () => {
document.getElementById(`c${n}`).textContent = `${ta.value.length}/${MAX_CHARS}`;
});
block.appendChild(ta);
return block;
}
for(let i=0;i<MAX_ENGAGEMENTS;i++){
engagementFieldsEl.appendChild(makeEngagementBlock(i));
}
// ———- Canvas drawing ———-
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const downloadLink = document.getElementById("downloadLink");
// Ribbon palette (feel free to change to match your brand)
const colors = [
"#2F3D73", // navy
"#6C7CB8", // slate blue
"#7DA8A1", // teal
"#F2A15D", // orange
"#B6654B", // rust
"#8B4B46", // maroon
"#C06B83", // rose
"#5F6B8A" // steel
];
function roundRect(ctx, x, y, w, h, r){
const radius = Math.min(r, h/2, w/2);
ctx.beginPath();
ctx.moveTo(x+radius, y);
ctx.arcTo(x+w, y, x+w, y+h, radius);
ctx.arcTo(x+w, y+h, x, y+h, radius);
ctx.arcTo(x, y+h, x, y, radius);
ctx.arcTo(x, y, x+w, y, radius);
ctx.closePath();
}
function drawNode(x, y, outerR=22, innerR=10){
// outer white ring
ctx.save();
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(x, y, outerR, 0, Math.PI*2);
ctx.fill();
// inner hole
ctx.fillStyle = "#0f172a";
ctx.beginPath();
ctx.arc(x, y, innerR, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines){
const words = text.split(/\s+/).filter(Boolean);
let line = "";
let lines = 0;
for (let i=0; i maxWidth && line){
ctx.fillText(line, x, y);
line = words[i];
y += lineHeight;
lines++;
if (maxLines && lines >= maxLines) return;
} else {
line = testLine;
}
}
if (line && (!maxLines || lines < maxLines)){
ctx.fillText(line, x, y);
}
}
function drawPathway({ prospect, items }){
// Background
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0,0,canvas.width,canvas.height);
// Title
ctx.fillStyle = "#6b7aa6";
ctx.font = "700 56px system-ui, -apple-system, Segoe UI, Roboto, Arial";
ctx.textAlign = "center";
ctx.fillText("Pathway to a Gift", canvas.width/2, 90);
ctx.fillStyle = "#111827";
ctx.font = "600 40px system-ui, -apple-system, Segoe UI, Roboto, Arial";
ctx.fillText(prospect || "Prospect Name", canvas.width/2, 140);
// Layout constants
const marginX = 90;
const topY = 210;
const segH = 120;
const gap = 26;
const ribbonW = canvas.width – marginX*2;
const radius = 60;
// Text styles
const monthFont = "800 34px system-ui, -apple-system, Segoe UI, Roboto, Arial";
const bodyFont = "600 24px system-ui, -apple-system, Segoe UI, Roboto, Arial";
for(let i=0; i<MAX_ENGAGEMENTS; i++){
const y = topY + i*(segH + gap);
const dirLTR = (i % 2 === 0); // alternate direction visually
const x = marginX;
const color = colors[i % colors.length];
// Ribbon segment
ctx.save();
ctx.fillStyle = color;
roundRect(ctx, x, y, ribbonW, segH, radius);
ctx.fill();
ctx.restore();
// Nodes at both ends (gives the “pathway” feel)
const leftNodeX = x;
const rightNodeX = x + ribbonW;
const nodeY = y + segH/2;
drawNode(leftNodeX, nodeY);
drawNode(rightNodeX, nodeY);
// Month/Year label placed at the "start side" (alternates L/R)
const month = (items[i]?.month || "").trim();
ctx.save();
ctx.font = monthFont;
ctx.fillStyle = "#111827";
ctx.textBaseline = "middle";
if(dirLTR){
ctx.textAlign = "left";
ctx.fillText(month || "", x – 10, nodeY);
} else {
ctx.textAlign = "right";
ctx.fillText(month || "", x + ribbonW + 10, nodeY);
}
ctx.restore();
// Engagement text
const engagement = (items[i]?.text || "").trim();
ctx.save();
ctx.font = bodyFont;
ctx.fillStyle = "#111827";
ctx.textBaseline = "top";
// Keep text away from nodes
const padLeft = 70;
const padRight = 70;
const textBoxX = x + padLeft;
const textBoxY = y + 26;
const textMaxW = ribbonW – padLeft – padRight;
const lineH = 30;
// If empty, don’t print placeholder lines (keeps it clean)
if(engagement){
// Wrap into up to ~3 lines
ctx.textAlign = "left";
wrapText(ctx, engagement, textBoxX, textBoxY, textMaxW, lineH, 3);
}
ctx.restore();
}
// Update download link
const dataUrl = canvas.toDataURL("image/png");
downloadLink.href = dataUrl;
statusEl.textContent = "Image generated.";
}
function getFormData(){
const prospect = document.getElementById("prospect").value.trim();
const items = [];
for(let i=1;i {
e.preventDefault();
const data = getFormData();
drawPathway(data);
});
document.getElementById(“fillExample”).addEventListener(“click”, () => {
document.getElementById(“prospect”).value = “Liangxiao Zhu”;
const example = [
{ month:”JAN 2023″, text:”Initial identification and early research, laying the foundation for future engagement.” },
{ month:”JAN 2025″, text:”Lead resurfaced and connected internally, ensuring the prospect was back on the radar.” },
{ month:”FEB 2025″, text:”Introductory visit confirmed philanthropic interest and alignment with priorities.” },
{ month:”JUN 2025″, text:”Campus visit and tour inspired a follow-up philanthropy conversation.” },
{ month:”JUN 2025″, text:”Proposal shaped and refined; naming opportunity details clarified.” },
{ month:”JUL 2025″, text:”Proposal presented; verbal commitments secured including giving society pledge.” },
{ month:”AUG 2025″, text:”Gift agreement drafted and finalized based on internal insights and details.” },
{ month:”SEP 2025″, text:”Agreement signed: scholarship established and naming gift secured.” },
];
for(let i=1;i {
document.getElementById(“prospect”).value = “”;
for(let i=1;i<=MAX_ENGAGEMENTS;i++){
document.getElementById(`m${i}`).value = "";
document.getElementById(`e${i}`).value = "";
document.getElementById(`c${i}`).textContent = `0/${MAX_CHARS}`;
}
statusEl.textContent = "";
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0,0,canvas.width,canvas.height);
downloadLink.href = "#";
});
// Draw a blank canvas on load
ctx.fillStyle = "#ffffff";
ctx.fillRect(0,0,canvas.width,canvas.height);