mevebu 1.10.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -21
- package/mevebu/app/mevebu.js +1 -1
- package/mevebu/app/tailwind.css +110 -17
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/csv.js +87 -0
- package/mevebu/libs/js/files.js +125 -0
- package/mevebu/libs/js/icons.js +2 -1
- package/mevebu/libs/js/pdf-engine.js +67 -0
- package/mevebu/libs/js/registry.js +253 -37
- package/mevebu/libs/js/tools/audio-converter.js +228 -0
- package/mevebu/libs/js/tools/csv-viewer.js +1 -51
- package/mevebu/libs/js/tools/data-dictionary.js +204 -0
- package/mevebu/libs/js/tools/image-to-pdf.js +251 -0
- package/mevebu/libs/js/tools/pdf-merge.js +158 -0
- package/mevebu/libs/js/tools/pdf-organizer.js +215 -0
- package/mevebu/libs/js/tools/pdf-split.js +198 -0
- package/mevebu/libs/js/tools/pdf-to-image.js +190 -0
- package/mevebu/libs/js/tools/pdf-to-text.js +172 -0
- package/mevebu/libs/js/tools/roster-generator.js +195 -0
- package/mevebu/libs/js/tools/sample-csv.js +252 -0
- package/mevebu/libs/js/tools/sql-insert.js +184 -0
- package/mevebu/libs/js/tools/svg-to-png.js +207 -0
- package/mevebu/libs/js/tools/team-generator.js +203 -0
- package/mevebu/libs/js/tools/text-to-pdf.js +182 -0
- package/mevebu/libs/js/tools/video-converter.js +316 -0
- package/package.json +1 -1
- package/mevebu/libs/js/tools/coin-flip.js +0 -78
- package/mevebu/libs/js/tools/name-picker.js +0 -68
- package/mevebu/libs/js/tools/spin-wheel.js +0 -168
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
// Spins through your options and lands on one. Canvas wheel with an eased
|
|
2
|
-
// spin, driven by the browser random source, so every option has equal odds.
|
|
3
|
-
import { el, toast, copyButton, randomInt, bench, stage, stack, panel, textArea, button, readPin, writePin, toolcard, actions } from "../core.js";
|
|
4
|
-
|
|
5
|
-
// The slice palette. A green ramp that alternates dark and light, so two
|
|
6
|
-
// neighbouring slices never read as one wedge and every label stays legible.
|
|
7
|
-
const SEGMENTS = ["#0f6b4a", "#e0f0e7", "#0a5138", "#bfcac1", "#35805f", "#1d3b2c", "#8fd0b3", "#0b2a1e"];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The slice label, in whichever ink reads better on that fill. Every fill in the
|
|
11
|
-
* palette sits far enough from mid grey that one of the two clears 4.5:1, so a
|
|
12
|
-
* pale slice never gets white text.
|
|
13
|
-
*/
|
|
14
|
-
function textOn(hex) {
|
|
15
|
-
const linear = (value) => (value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
|
|
16
|
-
const [r, g, b] = [1, 3, 5].map((at) => linear(parseInt(hex.slice(at, at + 2), 16) / 255));
|
|
17
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.2056 ? "#101a15" : "#FFFFFF";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function entriesFrom(text) {
|
|
21
|
-
return text.split(/\n+/).map((line) => line.trim()).filter(Boolean);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function mount(slot) {
|
|
25
|
-
const PIN = "spin-wheel";
|
|
26
|
-
const saved = readPin(PIN, { entries: "Ana\nBudi\nCitra\nDimas\nElena\nFajar" });
|
|
27
|
-
|
|
28
|
-
const listField = textArea({ value: saved.entries ?? "", "aria-label": "Options, one per line", placeholder: "One option per line" });
|
|
29
|
-
const canvas = el("canvas", { class: "h-auto w-[min(320px,100%)]", width: 360, height: 360, role: "img", "aria-label": "The wheel" });
|
|
30
|
-
const context = canvas.getContext("2d");
|
|
31
|
-
const historyBox = el("div", { class: "flex max-w-[340px] flex-wrap justify-center gap-1.5", role: "list" });
|
|
32
|
-
|
|
33
|
-
const spinButton = button("Spin", { icon: "rotate-cw", kind: "fill" });
|
|
34
|
-
const clearButton = button("Clear past picks", { icon: "trash", kind: "quiet" });
|
|
35
|
-
const copyWinner = copyButton(() => winner, "Copy winner");
|
|
36
|
-
|
|
37
|
-
const status = el("div", { class: "text-balance text-xl font-bold tracking-[-0.01em]", "aria-live": "polite" }, "Add options, then spin.");
|
|
38
|
-
|
|
39
|
-
let rotation = 0;
|
|
40
|
-
let busy = false;
|
|
41
|
-
let frame = 0;
|
|
42
|
-
let winner = "";
|
|
43
|
-
|
|
44
|
-
function draw() {
|
|
45
|
-
const entries = entriesFrom(listField.value);
|
|
46
|
-
const size = 360;
|
|
47
|
-
const center = size / 2;
|
|
48
|
-
const radius = center - 12;
|
|
49
|
-
const root = getComputedStyle(document.documentElement);
|
|
50
|
-
const ground = root.getPropertyValue("--void").trim() || "#fff";
|
|
51
|
-
const orbit = root.getPropertyValue("--orbit-hard").trim() || "#888";
|
|
52
|
-
const faint = root.getPropertyValue("--ink-3").trim() || "#888";
|
|
53
|
-
const jade = root.getPropertyValue("--jade").trim() || "#0f6b4a";
|
|
54
|
-
// Canvas text is outside the stylesheet, so it reads the same stack the page uses.
|
|
55
|
-
const stack = root.getPropertyValue("--sans").trim() || "system-ui, sans-serif";
|
|
56
|
-
|
|
57
|
-
context.clearRect(0, 0, size, size);
|
|
58
|
-
if (entries.length < 2) {
|
|
59
|
-
context.fillStyle = faint;
|
|
60
|
-
context.font = `15px ${stack}`;
|
|
61
|
-
context.textAlign = "center";
|
|
62
|
-
context.fillText(entries.length ? "Add one more option to spin" : "Type options to spin", center, center);
|
|
63
|
-
status.textContent = entries.length ? "One more option to go." : "Add options, then spin.";
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const slice = (Math.PI * 2) / entries.length;
|
|
67
|
-
entries.forEach((entry, i) => {
|
|
68
|
-
const a0 = rotation + i * slice;
|
|
69
|
-
context.beginPath();
|
|
70
|
-
context.moveTo(center, center);
|
|
71
|
-
context.arc(center, center, radius, a0, a0 + slice);
|
|
72
|
-
context.closePath();
|
|
73
|
-
context.fillStyle = SEGMENTS[i % SEGMENTS.length];
|
|
74
|
-
context.fill();
|
|
75
|
-
context.save();
|
|
76
|
-
context.translate(center, center);
|
|
77
|
-
context.rotate(a0 + slice / 2);
|
|
78
|
-
context.textAlign = "right";
|
|
79
|
-
context.fillStyle = textOn(SEGMENTS[i % SEGMENTS.length]);
|
|
80
|
-
context.font = `600 13px ${stack}`;
|
|
81
|
-
const label = entry.length > 13 ? `${entry.slice(0, 12)}…` : entry;
|
|
82
|
-
context.fillText(label, radius - 12, 4);
|
|
83
|
-
context.restore();
|
|
84
|
-
});
|
|
85
|
-
context.beginPath();
|
|
86
|
-
context.arc(center, center, 16, 0, Math.PI * 2);
|
|
87
|
-
context.fillStyle = ground;
|
|
88
|
-
context.fill();
|
|
89
|
-
context.strokeStyle = orbit;
|
|
90
|
-
context.stroke();
|
|
91
|
-
|
|
92
|
-
context.beginPath();
|
|
93
|
-
context.moveTo(center - 8, 2);
|
|
94
|
-
context.lineTo(center + 8, 2);
|
|
95
|
-
context.lineTo(center, 20);
|
|
96
|
-
context.closePath();
|
|
97
|
-
context.fillStyle = jade;
|
|
98
|
-
context.fill();
|
|
99
|
-
if (status.dataset.winner !== "1") status.textContent = "Add options, then spin.";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function spin() {
|
|
103
|
-
const entries = entriesFrom(listField.value);
|
|
104
|
-
if (entries.length < 2) return toast(entries.length ? "Add one more option to spin" : "Add options first");
|
|
105
|
-
if (busy) return;
|
|
106
|
-
busy = true;
|
|
107
|
-
spinButton.disabled = true;
|
|
108
|
-
status.dataset.winner = "";
|
|
109
|
-
const slice = (Math.PI * 2) / entries.length;
|
|
110
|
-
const winnerIndex = randomInt(entries.length);
|
|
111
|
-
const finish = (-Math.PI / 2) - (winnerIndex * slice + slice / 2);
|
|
112
|
-
const fullTurns = Math.PI * 2 * (5 + randomInt(3));
|
|
113
|
-
const delta = ((finish - rotation) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2) + fullTurns;
|
|
114
|
-
const start = rotation;
|
|
115
|
-
const duration = 2800 + randomInt(800);
|
|
116
|
-
const t0 = performance.now();
|
|
117
|
-
|
|
118
|
-
const step = (now) => {
|
|
119
|
-
const progress = Math.min(1, (now - t0) / duration);
|
|
120
|
-
rotation = start + delta * (1 - Math.pow(1 - progress, 4));
|
|
121
|
-
draw();
|
|
122
|
-
if (progress < 1) {
|
|
123
|
-
frame = requestAnimationFrame(step);
|
|
124
|
-
} else {
|
|
125
|
-
busy = false;
|
|
126
|
-
spinButton.disabled = false;
|
|
127
|
-
status.dataset.winner = "1";
|
|
128
|
-
winner = entries[winnerIndex];
|
|
129
|
-
status.textContent = winner;
|
|
130
|
-
const chip = el("span", { class: "rounded-full border border-orbit bg-deck-2 px-2.5 py-1 text-2xs font-semibold", role: "listitem" }, winner);
|
|
131
|
-
historyBox.prepend(chip);
|
|
132
|
-
while (historyBox.children.length > 12) historyBox.lastChild.remove();
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
frame = requestAnimationFrame(step);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
listField.addEventListener("input", () => {
|
|
139
|
-
status.dataset.winner = "";
|
|
140
|
-
winner = "";
|
|
141
|
-
writePin(PIN, { entries: listField.value });
|
|
142
|
-
draw();
|
|
143
|
-
});
|
|
144
|
-
spinButton.addEventListener("click", spin);
|
|
145
|
-
clearButton.addEventListener("click", () => {
|
|
146
|
-
historyBox.replaceChildren();
|
|
147
|
-
winner = "";
|
|
148
|
-
status.dataset.winner = "";
|
|
149
|
-
status.textContent = "Past picks cleared.";
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const card = toolcard({
|
|
153
|
-
body: bench(
|
|
154
|
-
stack(panel("Options, one per line", listField)),
|
|
155
|
-
stage(canvas, status, historyBox)
|
|
156
|
-
),
|
|
157
|
-
foot: actions(copyWinner, clearButton, spinButton)});
|
|
158
|
-
slot.append(card.node);
|
|
159
|
-
draw();
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
unmount() {
|
|
163
|
-
cancelAnimationFrame(frame);
|
|
164
|
-
busy = false;
|
|
165
|
-
}};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export default { mount };
|