started confetti

This commit is contained in:
Guy Sandler 2026-05-25 15:48:30 -07:00
parent 27cc48e4f9
commit 4880c05755
3 changed files with 189 additions and 39 deletions

View File

@ -228,6 +228,10 @@
<input type="checkbox" id="todo_hide_feedback" name="todo_hide_feedback">
<label for="todo_hide_feedback" class="sub-text" data-i18n="todo_hide_feedback">Hide Recent Feedback</label>
</div>
<div class="sub-option">
<input type="checkbox" id="todo_progress_rings" name="todo_progress_rings">
<label for="todo_progress_rings" class="sub-text">Show progress rings</label>
</div>
<div style="margin-top: 5px">
<span class="sub-text" data-i18n="max_items">Max items to show: </span><span
id="numTodoItems"></span>

View File

@ -543,6 +543,29 @@ function applyOptionsChanges(changes) {
} else {
window.location.reload();
}
break;
case "todo_progress_rings": {
// toggle progress rings immediately
const placeholder = document.getElementById("better-todo-progress-placeholder");
if (!placeholder) break;
if (changes[key].newValue === true || changes[key].newValue === undefined) {
if (typeof assignments?.then === 'function') {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.filter(item => {
const itemCourseId = parseInt(item.course_id || item.context_id || item?.plannable?.course_id);
return itemCourseId === courseId;
})
: data;
renderProgressRings(placeholder, scopedData);
});
}
} else {
placeholder.innerHTML = "";
}
break;
}
case "better_sidebar":
if (options.better_sidebar) {
ensureBetterSidebar();
@ -1799,7 +1822,7 @@ async function createTodoSections(location) {
else label = "<strong>" + key + "</strong>";
makeElement("div", wrapper, {
innerHTML: "<span>" + label + "</span>",
style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);" // TODO: might not be theme compatible
style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);"
})
let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
@ -1821,17 +1844,20 @@ async function createTodoSections(location) {
const feedbackElement = location.querySelector(".recent_feedback");
// populate progress rings placeholder
// populate progress rings placeholder (respect user toggle)
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder) {
if (options.todo_progress_rings === undefined || options.todo_progress_rings === true) {
renderProgressRings(progressPlaceholder, scopedData);
} else {
progressPlaceholder.innerHTML = "";
}
}
// Only show the Add Task control on the Assignments (tasks) tab.
if (betterTodoFilter === "tasks") {
ensureTodoTaskMenu(location, feedbackElement);
} else {
// remove the actions row if it exists when not on the assignments tab
const existing = location.querySelector("#better-todo-actions-row");
if (existing) existing.remove();
}
@ -2092,6 +2118,119 @@ function populateAnnouncements() {
});
}
function createConfettiBurst(targetElement, opts = {}) {
try {
const count = opts.count || 48;
const colors = opts.colors || ['#ff4d4f', '#ffc107', '#28a745', '#17a2b8', '#6f42c1', '#ff6b6b', '#ff8a65', '#ffd54f'];
const rect = targetElement.getBoundingClientRect();
const container = document.createElement('div');
container.className = 'bettercanvas-confetti-container';
container.style.position = 'fixed';
container.style.left = '0';
container.style.top = '0';
container.style.pointerEvents = 'none';
container.style.overflow = 'visible';
container.style.zIndex = '2147483647';
document.body.appendChild(container);
const originX = rect.left + rect.width / 2;
const originY = rect.top + rect.height * 0.35;
const particles = [];
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.className = 'bettercanvas-confetti';
const w = 4 + Math.floor(Math.random() * 7); // smaller pieces
const h = Math.max(3, Math.floor(w * (0.4 + Math.random() * 0.8)));
el.style.position = 'absolute';
el.style.width = w + 'px';
el.style.height = h + 'px';
el.style.background = colors[Math.floor(Math.random() * colors.length)];
el.style.left = (originX - w / 2) + 'px';
el.style.top = (originY - h / 2) + 'px';
el.style.opacity = '1';
el.style.borderRadius = Math.random() > 0.75 ? '50%' : '2px';
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.18)';
el.style.transformOrigin = 'center center';
el.style.willChange = 'transform, opacity';
container.appendChild(el);
const duration = 850 + Math.floor(Math.random() * 500);
const delay = Math.floor(Math.random() * 90);
const spread = opts.spread || 110;
const horizontalBias = (Math.random() - 0.5) * 2;
// Arc stays lower and wider than the old cone-shaped burst.
const endX = originX + horizontalBias * (spread * (0.7 + Math.random() * 0.6));
const endY = originY - (16 + Math.random() * 34);
const ctrlX = originX + horizontalBias * (spread * 0.25) + (Math.random() - 0.5) * 14;
const ctrlY = originY - (30 + Math.random() * 55);
particles.push({
el,
delay,
duration,
originX,
originY,
ctrlX,
ctrlY,
endX,
endY,
rotate: (Math.random() * 260) - 130,
scale: 0.8 + Math.random() * 0.5,
});
}
const startTime = performance.now();
let rafId = null;
const animate = now => {
let active = false;
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
const elapsed = now - startTime - particle.delay;
if (elapsed < 0) {
active = true;
continue;
}
const progress = Math.min(1, elapsed / particle.duration);
const eased = 1 - Math.pow(1 - progress, 3);
const x = (1 - eased) * (1 - eased) * particle.originX + 2 * (1 - eased) * eased * particle.ctrlX + eased * eased * particle.endX;
const y = (1 - eased) * (1 - eased) * particle.originY + 2 * (1 - eased) * eased * particle.ctrlY + eased * eased * particle.endY;
particle.el.style.transform = `translate(${Math.round(x - particle.originX)}px, ${Math.round(y - particle.originY)}px) rotate(${particle.rotate * eased}deg) scale(${particle.scale * (1 - eased * 0.15)})`;
particle.el.style.opacity = String(1 - progress);
if (progress < 1) {
active = true;
} else {
particle.el.remove();
particles.splice(i, 1);
}
}
if (active) {
rafId = requestAnimationFrame(animate);
} else {
try { container.remove(); } catch (e) { /* ignore */ }
if (rafId) cancelAnimationFrame(rafId);
}
};
rafId = requestAnimationFrame(animate);
// cleanup container after animations
setTimeout(() => {
try { container.remove(); } catch (e) { /* ignore */ }
}, 2400);
} catch (e) {
console.error('confetti error', e);
}
}
function markAs(item, element) {
const csrfToken = CSRFtoken();
const completeState = item.planner_override ? !item.planner_override.marked_complete : true;
@ -2117,9 +2256,14 @@ function markAs(item, element) {
element.style.transform = "translate(100%)";
element.style.opacity = "0";
// fire confetti only when marking complete (not when unmarking)
if (completeState) {
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
}
// update progress rings immediately so they animate while the item slides/fades
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder && typeof assignments?.then === 'function') {
if (progressPlaceholder && typeof assignments?.then === 'function' && (options.todo_progress_rings === undefined || options.todo_progress_rings === true)) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId

View File

@ -112,6 +112,7 @@ const defaultOptions = {
"tab_icons": false,
"todo_hide_feedback": false,
"todo_full_height": false,
"todo_progress_rings": true,
"device_dark": false,
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
// "show_updates": false,
@ -303,6 +304,7 @@ function setup() {
"gpa_calc_cumulative",
// /*'card_method_date',*/ "show_updates",
"todo_hide_feedback",
"todo_progress_rings",
"todo_full_height",
"device_dark",
"relative_dues",