// ==UserScript==
// @name Checkpoint Preview Reworked
// @namespace https://discord.com/users/1065604516399026176
// @version 1.0.0
// @description Checkpoint avatar preview and PNG exporter for Discord. Adds a button to the user panel (or press Ctrl+Shift+S).
// @author ItzMeShadow999
// @icon https://i.ibb.co/N24YY0PM/daba65d6-1fc5-45a4-b22e-d4d29cbbcf35.png
// @homepageURL https://github.com/ItzMeShadow999/CheckpointPreview-Reworked
// @supportURL https://github.com/ItzMeShadow999/CheckpointPreview-Reworked/issues
// @updateURL https://raw.githubusercontent.com/ItzMeShadow999/CheckpointPreview-Reworked/main/checkpoint-preview.user.js
// @downloadURL https://raw.githubusercontent.com/ItzMeShadow999/CheckpointPreview-Reworked/main/checkpoint-preview.user.js
// @match https://discord.com/*
// @match https://*.discord.com/*
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect i.ibb.co
// @run-at document-idle
// @noframes
// ==/UserScript==
(() => {
"use strict";
const W = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const KEY = "__CHECKPOINT_PREVIEW__";
const run = async (interactive) => {
window[KEY]?.destroy?.();
const D = {
"base": [[31, 16, 63, 102, 63, 102, 0.5, 7.0, 0], [168, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [305, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [442, 16, 65, 102, 65, 102, 0.0, 7.0, 0], [580, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [717, 16, 65, 102, 65, 102, 0.5, 7.0, 0], [855, 16, 63, 102, 63, 102, 0.5, 7.0, 0], [992, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [1130, 16, 63, 102, 63, 102, 0.5, 7.0, 0], [1266, 16, 65, 102, 65, 102, 0.5, 7.0, 0], [1404, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [1541, 16, 65, 102, 65, 102, 1.0, 7.0, 0], [1679, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [1816, 16, 64, 102, 64, 102, 0.5, 7.0, 0], [1954, 16, 63, 102, 63, 102, 0.5, 7.0, 0]],
"hat": [[28, 132, 69, 40, 69, 40, 0.5, -13.5, 0], [156, 144, 65, 57, 65, 57, 0.0, 7.5, 0], [276, 125, 73, 36, 73, 36, 6.5, -22.0, 0], [407, 141, 69, 32, 69, 32, -0.5, -8.0, 0], [547, 125, 31, 26, 31, 26, 5.5, -27.0, 0], [660, 125, 57, 25, 57, 25, 8.5, -28.5, 0], [781, 136, 65, 27, 65, 27, 20.5, -15.0, 0], [903, 131, 71, 39, 71, 39, 10.5, -15.0, 0], [1043, 142, 42, 17, 42, 17, 4.5, -14.0, 0], [1153, 125, 71, 46, 71, 46, 8.5, -19.0, 0], [1278, 126, 47, 35, 47, 35, 6.0, -21.5, 0], [1416, 125, 27, 29, 27, 29, 3.0, -26.0, 0], [1548, 134, 31, 21, 31, 21, 1.5, -20.5, 0], [1657, 142, 67, 23, 67, 23, -1.0, -11.5, 0]],
"face": [[50, 286, 35, 33, 35, 33, 6.5, 7.0, 0], [180, 286, 39, 20, 39, 20, 10.5, 0.5, 0], [305, 288, 39, 20, 39, 20, -10.5, 3.0, 0], [428, 276, 43, 43, 43, 43, 3.0, 3.0, 0], [555, 290, 39, 21, 39, 21, -10.5, 5.5, 0], [677, 281, 44, 31, 44, 31, 10.5, 2.0, 0], [802, 280, 45, 31, 45, 31, -9.5, 2.0, 0], [931, 285, 37, 27, 37, 27, 8.0, 3.5, 0], [1049, 279, 56, 32, 56, 32, -4.5, 0.0, 0], [1177, 272, 41, 41, 41, 41, 9.0, -2.0, 0], [1306, 285, 42, 32, 42, 32, 12.0, 6.0, 0], [1425, 277, 64, 36, 64, 36, 2.0, 0.0, 0], [1557, 284, 38, 24, 38, 24, -10.0, 1.0, 0], [1680, 284, 42, 28, 42, 28, 10.5, 2.5, 0], [1804, 279, 44, 38, 44, 38, 9.0, 3.5, 0]],
"outfit": [[39, 444, 48, 45, 48, 45, 1.0, -6.0, 0], [170, 444, 55, 46, 55, 46, 1.0, -6.0, 0], [314, 445, 46, 46, 46, 46, 0.5, -4.5, 0], [451, 446, 63, 42, 63, 42, 7.5, -5.5, 0], [588, 448, 50, 39, 50, 39, 1.5, -5.0, 0], [725, 446, 51, 41, 51, 41, 2.0, -6.0, 0], [864, 447, 46, 41, 46, 41, 0.5, -5.0, 0], [1000, 448, 49, 39, 49, 39, 1.5, -5.0, 0], [1137, 448, 50, 39, 50, 39, 1.0, -5.0, 0], [1275, 448, 49, 39, 49, 39, 1.0, -5.0, 0], [1406, 436, 56, 52, 56, 52, -1.5, -10.0, 0], [1549, 444, 50, 46, 50, 46, 2.0, -6.0, 0], [1686, 448, 49, 40, 49, 40, 0.0, -5.0, 0], [1825, 447, 51, 40, 51, 40, 0.5, -5.5, 0], [1962, 447, 46, 41, 46, 41, 0.5, -5.0, 0], [40, 572, 46, 42, 46, 42, 1.0, -4.5, 0], [176, 572, 47, 40, 47, 40, 0.5, -5.5, 0], [314, 572, 46, 41, 46, 41, 0.0, -5.0, 0], [451, 569, 49, 46, 49, 46, 1.0, -6.0, 0], [588, 569, 49, 45, 49, 45, 1.0, -6.0, 0], [725, 571, 51, 41, 51, 41, 2.0, -6.0, 0], [865, 573, 44, 39, 44, 39, 1.0, -5.5, 0], [1001, 572, 45, 42, 45, 42, 0.5, -4.5, 0], [1138, 571, 62, 42, 62, 42, 8.0, -5.5, 0], [1276, 573, 45, 39, 45, 39, 0.0, -5.5, 0], [1406, 562, 56, 51, 56, 51, -1.5, -10.0, 0], [1551, 573, 45, 39, 45, 39, 1.0, -5.5, 0], [1686, 573, 49, 40, 49, 40, 0.0, -5.0, 0], [1825, 572, 47, 40, 47, 40, 0.5, -5.5, 0], [1962, 572, 46, 42, 46, 42, 0.5, -4.5, 0], [39, 695, 48, 44, 48, 44, 0.5, -6.0, 0], [173, 690, 53, 49, 53, 49, 0.5, -8.0, 0], [315, 698, 44, 39, 44, 39, 0.0, -5.0, 0], [450, 697, 49, 42, 49, 42, 0.0, -4.0, 0], [591, 698, 45, 40, 45, 40, 2.0, -4.5, 0], [728, 698, 46, 40, 46, 40, 2.5, -4.5, 0], [862, 698, 48, 40, 48, 40, 0.0, -4.5, 0], [1001, 697, 62, 41, 62, 41, 8.5, -5.0, 0], [1138, 697, 51, 41, 51, 41, 0.0, -4.5, 0], [1274, 698, 48, 40, 48, 40, -0.5, -4.5, 0], [1412, 695, 48, 44, 48, 44, 0.5, -6.0, 0], [1549, 697, 51, 41, 51, 41, 2.0, -5.0, 0], [1688, 697, 61, 41, 61, 41, 8.5, -5.0, 0], [1826, 696, 45, 45, 45, 45, 1.0, -4.0, 0], [1962, 698, 46, 41, 46, 41, 0.0, -4.5, 0], [38, 823, 49, 41, 49, 41, 0.5, -4.0, 0], [177, 823, 45, 42, 45, 42, 0.0, -4.0, 0], [312, 823, 50, 41, 50, 41, 0.5, -4.0, 0], [452, 823, 45, 41, 45, 41, 0.0, -4.0, 0], [591, 824, 45, 39, 45, 39, 2.0, -4.0, 0], [726, 820, 48, 44, 48, 44, 1.0, -5.5, 0], [863, 822, 50, 41, 50, 41, 2.0, -5.0, 0], [1003, 824, 45, 39, 45, 39, 2.0, -4.0, 0], [1138, 821, 46, 46, 46, 46, 0.5, -3.5, 0], [1274, 823, 49, 41, 49, 41, 0.0, -4.0, 0], [1413, 822, 62, 42, 62, 42, 8.5, -5.0, 0], [1550, 820, 48, 44, 48, 44, 1.0, -5.5, 0], [1688, 821, 46, 46, 46, 46, 1.5, -3.5, 0], [1818, 812, 56, 52, 56, 52, -2.0, -9.5, 0], [1961, 823, 49, 41, 49, 41, 0.5, -4.0, 0], [39, 948, 49, 40, 49, 40, 1.5, -4.0, 0], [175, 949, 48, 39, 48, 39, -0.5, -4.0, 0], [315, 948, 44, 39, 44, 39, 0.0, -4.5, 0], [451, 948, 47, 40, 47, 40, 0.0, -4.5, 0], [588, 945, 48, 44, 48, 44, 0.5, -5.5, 0], [728, 949, 46, 39, 46, 39, 2.5, -4.0, 0], [857, 937, 56, 52, 56, 52, -1.0, -9.5, 0], [1001, 948, 45, 42, 45, 42, 0.5, -4.0, 0], [1138, 946, 46, 46, 46, 46, 0.5, -3.5, 0], [1274, 947, 51, 41, 51, 41, 1.0, -5.0, 0]],
"shoes": [[43, 1109, 40, 14, 40, 14, 0.5, 6.0, 0], [167, 1109, 42, 12, 42, 12, -1.5, 4.5, 0], [294, 1107, 38, 14, 38, 14, 11.0, 4.5, 0], [417, 1110, 41, 11, 41, 11, 11.5, 5.5, 0], [542, 1108, 41, 13, 41, 13, 11.5, 4.5, 0], [668, 1110, 40, 11, 40, 11, -9.0, 5.5, 0], [792, 1110, 42, 10, 42, 10, 12.0, 5.0, 0], [919, 1109, 39, 13, 39, 13, 9.5, 5.5, 0], [1044, 1109, 40, 12, 40, 12, 12.0, 5.0, 0], [1167, 1104, 43, 16, 43, 16, -13.5, 2.0, 0], [1293, 1107, 42, 13, 42, 13, -10.0, 3.5, 0], [1418, 1109, 40, 11, 40, 11, 10.0, 4.5, 0], [1543, 1110, 41, 11, 41, 11, 11.5, 5.5, 0], [1665, 1107, 47, 14, 47, 14, 12.5, 4.0, 0]],
"wearable": [[26, 1220, 25, 26, 25, 26, 0.5, 40.5, 0], [159, 1220, 19, 26, 19, 26, 1.0, 41.0, 0], [277, 1207, 32, 24, 32, 24, 7.5, 27.0, 0], [410, 1222, 18, 19, 18, 19, 0.0, 38.5, 0], [535, 1220, 19, 22, 19, 22, 0.5, 38.5, 0], [631, 1177, 46, 52, 46, 52, 5.0, 11.5, 0], [768, 1181, 37, 54, 37, 54, 5.0, 17.5, 0], [902, 1194, 26, 43, 26, 43, 5.0, 23.0, 0], [1062, 1196, 42, 38, 42, 38, -12.9, 22.7, 0], [1138, 1197, 102, 28, 102, 28, 0.0, 18.0, 1], [1266, 1188, 95, 30, 95, 30, 29.8, 10.0, 1], [1383, 1159, 112, 79, 112, 79, -1.0, 6.0, 1], [1504, 1137, 120, 109, 120, 109, -0.5, -0.5, 1], [1657, 1143, 64, 51, 64, 51, -27.0, -24.5, 0]],
"aura": [[13, 1360, 98, 16, 98, 16, 4.0, 3.0, 0], [128, 1249, 118, 127, 118, 127, -21.0, -2.5, 0], [250, 1251, 126, 121, 126, 121, 15.5, -2.0, 0], [379, 1251, 118, 110, 118, 110, -30.0, -0.5, 0], [503, 1251, 119, 119, 119, 119, 17.0, -1.5, 0], [632, 1294, 117, 80, 117, 80, -2.5, 10.0, 0], [760, 1251, 106, 118, 106, 118, 2.5, 0.0, 0], [900, 1300, 92, 76, 92, 76, 11.0, 12.0, 0], [1001, 1251, 124, 125, 124, 125, -29.0, 9.0, 0], [1128, 1265, 121, 103, 121, 103, 15.5, 4.0, 0], [1254, 1258, 119, 110, 119, 110, -5.5, 2.5, 0], [1386, 1316, 109, 60, 109, 60, 13.5, 41.0, 0], [1506, 1317, 116, 59, 116, 59, -2.0, 34.5, 0], [1627, 1307, 124, 68, 124, 68, -26.0, 28.0, 0]]
};
const CATS = ["base", "face", "outfit", "hat", "shoes", "wearable", "aura"];
const LABEL = {
base: "Base",
face: "Face",
outfit: "Outfit",
hat: "Head",
shoes: "Shoes",
wearable: "Accessory",
aura: "Aura"
};
const OPTIONAL = new Set(["hat", "outfit", "shoes", "wearable", "aura"]);
const DEFAULT = {base: 14, face: 4, outfit: 55, hat: 7, shoes: 0, wearable: 11, aura: null};
const DEFAULT_ADJ = {
"wearable:6": {x: 36.9, y: 0.6, s: 1, r: 32},
"wearable:2": {x: -0.1, y: 0, s: 1, r: 0}
};
const ANCHOR = {
hat: [31.5, 31],
face: [31.5, 28],
outfit: [31, 78],
shoes: [31.5, 88.6],
wearable: [31.2, 43.2],
aura: [31.2, 43.2]
};
const CENTERED = new Set(["hat", "face", "shoes", "aura"]);
const VIEW = {x: -50.25, y: -32, s: 164};
const AURA_ALPHA = 1;
const REFERENCE = {width: 2048, height: 1376};
const ORIGINAL = {width: 8380, height: 5632};
const CREDIT_NAME = "literally.shadow";
const CREDIT_URL = "https://discord.com/users/1065604516399026176";
const CREDIT = "by " + CREDIT_NAME;
const CREDIT_LINK = `by ${CREDIT_NAME}`;
const pageTheme = () => {
const cl = document.documentElement.classList, bl = document.body?.classList;
const has = (n) => cl.contains(n) || bl?.contains(n);
if (has("theme-light")) return "light";
if (has("theme-dark") || has("theme-darker") || has("theme-midnight")) return "dark";
return window.matchMedia?.("(prefers-color-scheme: light)").matches ? "light" : "dark";
};
const BGS = {
transparent: null, escuro: "#313338", claro: "#f2f3f5", blurple: "#5865f2",
onyx: "#1e1f22", midnight: "#0b0d14", black: "#000000", white: "#ffffff", grey: "#4e5058",
green: "#23a55a", teal: "#1abc9c", yellow: "#f0b232", red: "#da373c", fuchsia: "#eb459f",
sunset: {angle: 160, grad: ["#ff512f", "#dd2476"]},
ocean: {angle: 160, grad: ["#2193b0", "#6dd5ed"]},
aurora: {angle: 150, grad: ["#00c9ff", "#92fe9d"]},
candy: {angle: 135, grad: ["#f093fb", "#f5576c"]},
ember: {angle: 160, grad: ["#f12711", "#f5af19"]},
twilight: {angle: 160, grad: ["#232526", "#5865f2"]},
custom: null,
customgrad: null
};
const BG_MENU = [
["Basic", [["transparent", "Transparent"], ["escuro", "Dark"], ["claro", "Light"], ["blurple", "Blurple"]]],
["Colors", [["onyx", "Onyx"], ["midnight", "Midnight"], ["black", "Black"], ["white", "White"], ["grey", "Grey"], ["green", "Green"], ["teal", "Teal"], ["yellow", "Yellow"], ["red", "Red"], ["fuchsia", "Fuchsia"]]],
["Gradients", [["sunset", "Sunset"], ["ocean", "Ocean"], ["aurora", "Aurora"], ["candy", "Candy"], ["ember", "Ember"], ["twilight", "Twilight"]]]
];
const BG_LABEL = {custom: "Custom color", customgrad: "Custom gradient", image: "Image"};
for (const [, items] of BG_MENU) for (const [k, l] of items) BG_LABEL[k] = l;
function paintBackground(cx, size, bg) {
if (!bg) return;
if (typeof bg === "string") {
cx.fillStyle = bg;
cx.fillRect(0, 0, size, size);
} else if (bg.grad) {
const a = bg.angle * Math.PI / 180, dx = Math.sin(a), dy = -Math.cos(a);
const half = (Math.abs(size * dx) + Math.abs(size * dy)) / 2, m = size / 2;
const g = cx.createLinearGradient(m - dx * half, m - dy * half, m + dx * half, m + dy * half);
bg.grad.forEach((c, i) => g.addColorStop(i / (bg.grad.length - 1), c));
cx.fillStyle = g;
cx.fillRect(0, 0, size, size);
} else if (bg.img) {
const w = bg.img.width, h = bg.img.height;
if (!w || !h) return;
cx.imageSmoothingEnabled = true;
cx.imageSmoothingQuality = "high";
if (bg.fit === "stretch") cx.drawImage(bg.img, 0, 0, size, size);
else {
const k = bg.fit === "contain" ? Math.min(size / w, size / h) : Math.max(size / w, size / h);
cx.drawImage(bg.img, (size - w * k) / 2, (size - h * k) / 2, w * k, h * k);
}
}
}
const HAT_FIX = {3: [0, -9], 4: [0, 9], 6: [0, 16], 8: [0, 2], 10: [0, -7], 11: [-8.9, -7.9]};
const HAND = [13.3, 78.9];
const WEAR = {
0: {t: "hand", g: [0.5, 0.05]},
1: {t: "hand", g: [0.5, 0.45]},
2: {t: "hand", g: [0.537, 0.615]},
3: {t: "hand", g: [0.5, 0.04]},
4: {t: "hand", g: [0.5, 0.04]},
5: {t: "hand", g: [0.835, 0.856]},
6: {t: "hand", g: [0.712, 0.73]},
7: {t: "hand", g: [0.606, 0.648]},
8: {t: "tail", g: [0.06, 0.82], at: [29, 85]},
9: {t: "wing", at: [31.7, 60]},
10: {t: "wing", at: [31.7, 60]},
11: {t: "wing", at: [31.7, 54]},
12: {t: "wing", at: [31.7, 50]},
13: {t: "front", at: [31.7, 46]},
};
const isBack = (c, i) => c === "wearable" && (WEAR[i].t === "wing" || WEAR[i].t === "tail");
const originalPlace = (c, i, s) => {
const m = D[c][i];
if (c === "base") return {u: m[4] / 2, v: m[5] / 2, px: 0, py: 0};
if (c === "wearable") {
const w = WEAR[i];
if (w.t === "hand" || w.t === "tail") {
const p = w.t === "hand" ? HAND : w.at, px = (w.g[0] - 0.5) * m[4], py = (w.g[1] - 0.5) * m[5];
return {u: p[0] - px, v: p[1] - py, px, py};
}
return {u: w.at[0], v: w.at[1], px: 0, py: 0};
}
if (c === "shoes") {
const bt = srcData(c, i).bb, floor = D.base[s.base][5] + 0.2;
return {u: 31.5, v: floor - (bt - 0.5) * m[5], px: 0, py: 0};
}
if (c === "hat") {
const f = HAT_FIX[i] || [0, 0];
return {u: 31.5 + f[0], v: ANCHOR.hat[1] + m[7] + f[1], px: 0, py: 0};
}
const a = ANCHOR[c], ox = CENTERED.has(c) ? 0 : m[6];
return {u: a[0] + ox, v: a[1] + m[7], px: 0, py: 0};
};
const CALIBRATION = {
"face:5": {"x": 1.9, "y": 0, "s": 1, "r": 0},
"outfit:30": {"x": 2.3, "y": 4.4, "s": 1, "r": 0},
"hat:11": {"x": 0, "y": 5.8, "s": 1, "r": 0},
"hat:0": {"x": 2.3, "y": -6.2, "s": 1, "r": 0},
"shoes:2": {"x": 2, "y": 1.3, "s": 1, "r": 0},
"shoes:5": {"x": 2, "y": 1.3, "s": 1, "r": 0},
"wearable:4": {"x": 2.3, "y": 1.2, "s": 1, "r": 0},
"wearable:6": {"x": 1.5, "y": 0.6, "s": 1, "r": 0}
};
const REFERENCE_ITEM = {face: 5, outfit: 30, hat: 0, shoes: 2, wearable: 4};
const shift = (p, a) => ({...p, u: p.u + a.x, v: p.v + a.y});
function sourceCenter(c, i) {
const sprite = srcData(c, i), m = D[c][i], [x, y] = cellBounds(c, i);
return {
u: (m[0] - x + sprite.w / 2) / SOURCE_SCALE.x,
v: (m[1] - y + sprite.h / 2) / SOURCE_SCALE.y
};
}
function referenceOrigin(c, s) {
const id = REFERENCE_ITEM[c], center = sourceCenter(c, id);
const target = shift(originalPlace(c, id, s), CALIBRATION[c + ":" + id]);
return {u: target.u - center.u, v: target.v - center.v};
}
const place = (c, i, s) => {
srcData(c, i);
if (c === "shoes") srcData("base", s.base);
const p = originalPlace(c, i, s), exact = CALIBRATION[c + ":" + i];
if (exact) return shift(p, exact);
if (c === "base") return p;
if (c === "hat" || c === "outfit" || c === "aura") {
const origin = referenceOrigin(c === "aura" ? "outfit" : c, s), center = sourceCenter(c, i);
return {...p, u: origin.u + center.u, v: origin.v + center.v};
}
if (c === "wearable" && WEAR[i].t !== "hand") {
const origin = referenceOrigin("outfit", s), center = sourceCenter("base", s.base);
const base = originalPlace("base", s.base, s);
return shift(p, {x: origin.u - (base.u - center.u), y: origin.v - (base.v - center.v)});
}
return shift(p, CALIBRATION[c + ":" + REFERENCE_ITEM[c]]);
};
const NOADJ = {x: 0, y: 0, s: 1, r: 0};
const ADJ = JSON.parse(JSON.stringify(DEFAULT_ADJ));
const getAdj = (c, id) => ADJ[c + ":" + id] || NOADJ;
const ATLAS_DATA_URI = "";
const ATLAS_URL = "https://i.ibb.co/YFMcML2D/Checkpoint-Assets.png";
const loadImg = (src) => new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image blocked or failed to load"));
img.src = src;
});
const gmBlob = (url) => new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== "function") return reject(new Error("GM_xmlhttpRequest unavailable"));
GM_xmlhttpRequest({
method: "GET", url, responseType: "blob", timeout: 60000,
onload: (r) => (r.status >= 200 && r.status < 400 && r.response ? resolve(r.response) : reject(new Error("HTTP " + r.status))),
onerror: () => reject(new Error("GM request failed")),
ontimeout: () => reject(new Error("GM request timed out"))
});
});
async function fetchBitmap(src) {
if (src.startsWith("data:")) return createImageBitmap(await loadImg(src));
try {
return await createImageBitmap(await gmBlob(src));
} catch (e) {
console.warn("[Checkpoint Preview Reworked] GM fetch failed, trying page fetch:", e);
}
try {
const res = await fetch(src, {mode: "cors"});
if (!res.ok) throw new Error("HTTP " + res.status);
return await createImageBitmap(await res.blob());
} catch (e) {
return createImageBitmap(await loadImg(src));
}
}
async function loadAtlas() {
const bitmap = await fetchBitmap(ATLAS_DATA_URI || ATLAS_URL);
if (bitmap.width !== ORIGINAL.width || bitmap.height !== ORIGINAL.height) {
const {width, height} = bitmap;
bitmap.close();
throw new Error(`Expected ${ORIGINAL.width} × ${ORIGINAL.height}, got ${width} × ${height}`);
}
return bitmap;
}
async function tryCspOverride() {
try {
const csp = W.VencordNative?.csp;
if (!csp?.requestAddOverride) return null;
return await csp.requestAddOverride("https://i.ibb.co/", ["connect-src", "img-src"], "Checkpoint Preview Reworked");
} catch (e) {
return null;
}
}
async function decodeAtlas(file) {
const header = await file.slice(0, 24).arrayBuffer();
const signature = new Uint8Array(header), png = [137, 80, 78, 71, 13, 10, 26, 10];
if (signature.length < 24 || png.some((v, i) => signature[i] !== v) ||
signature[12] !== 73 || signature[13] !== 72 || signature[14] !== 68 || signature[15] !== 82) {
throw new Error("Select the original Checkpoint PNG file.");
}
const view = new DataView(header), width = view.getUint32(16), height = view.getUint32(20);
if (width !== ORIGINAL.width || height !== ORIGINAL.height) {
throw new Error(`Expected the original ${ORIGINAL.width} × ${ORIGINAL.height} PNG. Selected: ${width} × ${height} px. Choose the full-size file from your computer.`);
}
const bitmap = await createImageBitmap(file);
if (bitmap.width !== width || bitmap.height !== height) {
bitmap.close();
throw new Error("The PNG could not be decoded at its original dimensions.");
}
return bitmap;
}
function chooseAtlas(note) {
return new Promise((resolve) => {
const loader = document.createElement("div");
loader.style.cssText = "position:fixed;inset:0;z-index:2147483000";
const ui = loader.attachShadow({mode: "open"});
loader.setAttribute("data-theme", pageTheme());
ui.innerHTML = `
Checkpoint Asset · Open PNG
Select the original Checkpoint Asset PNG file, at ${ORIGINAL.width} × ${ORIGINAL.height} px.
The image stays on your computer. Items keep their original size.
`;
const input = ui.querySelector("input"), select = ui.querySelector(".primary"), status = ui.querySelector(".status");
let settled = false, reading = false;
if (note) {
status.classList.add("error");
status.textContent = note;
}
const finish = (bitmap) => {
if (settled) {
bitmap?.close();
return;
}
settled = true;
window.removeEventListener("keydown", onKey, true);
loader.remove();
if (window[KEY] === pending) delete window[KEY];
resolve(bitmap);
};
const onKey = (e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopImmediatePropagation();
finish(null);
}
};
const pending = {destroy: () => finish(null)};
window[KEY] = pending;
select.addEventListener("click", () => input.click());
ui.querySelector(".cancel").addEventListener("click", () => finish(null));
ui.querySelector(".overlay").addEventListener("click", (e) => {
if (e.target === e.currentTarget) finish(null);
});
input.addEventListener("change", async () => {
const file = input.files?.[0];
input.value = "";
if (!file || settled || reading) return;
reading = true;
select.disabled = true;
status.classList.remove("error");
status.textContent = "Opening PNG…";
try {
finish(await decodeAtlas(file));
} catch (e) {
if (!settled) {
status.classList.add("error");
status.textContent = e.message || "Could not open this PNG. Please select the original file.";
}
} finally {
reading = false;
if (!settled) select.disabled = false;
}
});
for (const event of ["keydown", "keyup", "keypress"]) ui.addEventListener(event, (e) => e.stopPropagation());
window.addEventListener("keydown", onKey, true);
document.body.appendChild(loader);
select.focus();
});
}
let atlas = null, cspResult = null;
try {
atlas = await loadAtlas();
} catch (e) {
console.warn("[Checkpoint Preview Reworked] Auto-load failed (likely Discord CSP):", e);
cspResult = await tryCspOverride();
}
if (!atlas && interactive) {
atlas = await chooseAtlas(cspResult === "ok"
? "i.ibb.co was whitelisted. Reload Discord and run the script again, or select the PNG manually now."
: "Couldn't auto-load the image (Discord's CSP blocks i.ibb.co). Select the PNG manually instead.");
}
if (!atlas) return;
const SOURCE_SCALE = {x: atlas.width / REFERENCE.width, y: atlas.height / REFERENCE.height};
const PREVIEW = Math.ceil(VIEW.s * Math.max(SOURCE_SCALE.x, SOURCE_SCALE.y));
let destroyed = false;
const yieldUI = () => new Promise((r) => setTimeout(r));
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
const srcCache = new Map();
const scan = new OffscreenCanvas(1, 1), scanContext = scan.getContext("2d", {willReadFrequently: true});
const CELL = ORIGINAL.height / 11;
const SPACED_STRIDE = (ORIGINAL.width - CELL) / 14;
const ROWS = {base: 0, hat: 1, face: 2, shoes: 8, wearable: 9, aura: 10};
function cellBounds(c, i) {
const column = c === "outfit" ? i % 15 : i;
const row = c === "outfit" ? 3 + Math.floor(i / 15) : ROWS[c];
const stride = c === "base" || c === "outfit" ? SPACED_STRIDE : CELL;
return [column * stride, row * CELL, CELL, CELL];
}
const srcData = (c, i) => {
const key = c + ":" + i;
let hit = srcCache.get(key);
if (hit) return hit;
const [sx, sy, sw, sh] = cellBounds(c, i);
if (scan.width !== sw) scan.width = sw;
if (scan.height !== sh) scan.height = sh;
scanContext.clearRect(0, 0, sw, sh);
scanContext.imageSmoothingEnabled = false;
scanContext.drawImage(atlas, sx, sy, sw, sh, 0, 0, sw, sh);
const pixels = scanContext.getImageData(0, 0, sw, sh).data;
let left = sw, top = sh, right = -1, bottom = -1, sole = -1;
for (let y = 0; y < sh; y++) {
for (let x = 0; x < sw; x++) {
const alpha = pixels[(y * sw + x) * 4 + 3];
if (!alpha) continue;
if (x < left) left = x;
if (x > right) right = x;
if (y < top) top = y;
if (y > bottom) bottom = y;
if (alpha > 60) sole = y;
}
}
if (right < left || bottom < top) throw new Error(`Item ${LABEL[c]} ${i + 1} is missing. Use the original Checkpoint sheet.`);
const x = sx + left, y = sy + top, w = right - left + 1, h = bottom - top + 1;
const cv = new OffscreenCanvas(w, h), cx = cv.getContext("2d");
cx.imageSmoothingEnabled = false;
cx.drawImage(atlas, x, y, w, h, 0, 0, w, h);
const m = D[c][i];
m[0] = x;
m[1] = y;
m[2] = w;
m[3] = h;
m[4] = w / SOURCE_SCALE.x;
m[5] = h / SOURCE_SCALE.y;
hit = {cv, w, h, bb: sole >= top ? (sole - top + 1) / h : 1};
srcCache.set(key, hit);
return hit;
};
async function compose(sel, {bg = null, isStale = () => false} = {}) {
const size = PREVIEW;
sel = {...sel};
if (destroyed || isStale()) return null;
const back = sel.wearable != null && isBack("wearable", sel.wearable);
const order = [["aura", sel.aura], ...(back ? [["wearable", sel.wearable]] : []), ["base", sel.base], ["outfit", sel.outfit], ["face", sel.face], ["hat", sel.hat], ["shoes", sel.shoes], ...(!back ? [["wearable", sel.wearable]] : [])].filter(([, id]) => id != null);
const parts = [];
for (const [c, id] of order) {
await yieldUI();
if (destroyed || isStale()) return null;
parts.push([c, id, srcData(c, id)]);
}
const cv = new OffscreenCanvas(size, size), cx = cv.getContext("2d");
cx.imageSmoothingEnabled = true;
cx.imageSmoothingQuality = "high";
paintBackground(cx, size, bg);
for (const [c, id, s] of parts) {
const p = place(c, id, sel), a = getAdj(c, id);
const fx = (p.u + a.x - VIEW.x) * SOURCE_SCALE.x, fy = (p.v + a.y - VIEW.y) * SOURCE_SCALE.y;
cx.globalAlpha = c === "aura" ? AURA_ALPHA : 1;
if (!a.r) cx.drawImage(s.cv, Math.round(fx) - Math.round(s.w / 2), Math.round(fy) - Math.round(s.h / 2)); else {
const qx = p.px * SOURCE_SCALE.x, qy = p.py * SOURCE_SCALE.y;
cx.save();
cx.translate(fx + qx, fy + qy);
cx.rotate(a.r * Math.PI / 180);
cx.drawImage(s.cv, -s.w / 2 - qx, -s.h / 2 - qy);
cx.restore();
}
}
cx.globalAlpha = 1;
return cv;
}
const sel = {...DEFAULT};
const S = {cat: "outfit", bg: "transparent", bgColor: "#5865f2", gradA: "#ff512f", gradB: "#dd2476", gradAngle: 160, bgImg: null, bgThumb: "", bgFit: "cover"};
function bgSpec() {
if (S.bg === "custom") return S.bgColor;
if (S.bg === "customgrad") return {angle: S.gradAngle, grad: [S.gradA, S.gradB]};
if (S.bg === "image") return S.bgImg ? {img: S.bgImg, fit: S.bgFit} : null;
return BGS[S.bg] ?? null;
}
const rnd = (c) => Math.floor(Math.random() * D[c].length);
const maybe = (c, p) => (Math.random() < p ? null : rnd(c));
const randomize = () => Object.assign(sel, {
base: rnd("base"),
face: rnd("face"),
outfit: rnd("outfit"),
hat: maybe("hat", 0.25),
shoes: maybe("shoes", 0.12),
wearable: maybe("wearable", 0.35),
aura: maybe("aura", 0.55)
});
const host = document.createElement("div");
host.style.cssText = "position:fixed;inset:0;z-index:2147483000;pointer-events:none";
const root = host.attachShadow({mode: "open"});
root.innerHTML = `
Checkpoint Preview Reworked
Ctrl + Shift + S${CREDIT_LINK}
Rendering…
`;
document.body.appendChild(host);
const applyTheme = () => host.setAttribute("data-theme", pageTheme());
applyTheme();
const themeObserver = new MutationObserver(applyTheme);
themeObserver.observe(document.documentElement, {attributes: true, attributeFilter: ["class"]});
themeObserver.observe(document.body, {attributes: true, attributeFilter: ["class"]});
const $ = (s) => root.querySelector(s);
const wrap = $(".wrap"), pv = $("canvas.pv"), pctx = pv.getContext("2d"), busy = $(".busy"), grid = $(".grid");
const view = $(".view");
const fitPreview = () => {
const side = Math.floor(Math.min(PREVIEW, view.clientWidth, view.clientHeight));
if (side > 0) {
pv.style.width = side + "px";
pv.style.height = side + "px";
}
};
const previewObserver = new ResizeObserver(fitPreview);
previewObserver.observe(view);
fitPreview();
pv.title = `${PREVIEW} × ${PREVIEW} px PNG · Original ${atlas.width} × ${atlas.height} px source`;
let toastT, visible = interactive;
const toast = (t) => {
const e = $(".toast");
e.textContent = t;
e.classList.add("on");
clearTimeout(toastT);
toastT = setTimeout(() => e.classList.remove("on"), 2600);
};
const tip = $(".tip"), tipText = tip.querySelector(".tt");
let tipEl = null, tipFollow = false;
const tipHide = () => {
tipEl = null;
tipFollow = false;
tip.classList.remove("on");
};
const tipPlace = (e) => {
if (!tipEl) return;
let r = tipEl.getBoundingClientRect();
if (tipFollow) r = {left: e.clientX, right: e.clientX, top: e.clientY - 6, bottom: e.clientY + 14, width: 0, height: 20};
const w = tip.offsetWidth, h = tip.offsetHeight, vw = window.innerWidth, gap = 10;
const cx = r.left + (r.right - r.left) / 2;
const left = Math.max(8, Math.min(vw - w - 8, cx - w / 2));
const below = r.top - h - gap < 8;
tip.classList.toggle("below", below);
tip.style.left = Math.round(left) + "px";
tip.style.top = Math.round(below ? r.bottom + gap : r.top - h - gap) + "px";
tip.style.setProperty("--ax", Math.round(Math.max(14, Math.min(w - 14, cx - left))) + "px");
};
root.addEventListener("mouseover", (e) => {
const el = e.target.closest?.("[title],[data-tip]");
if (!el) return tipHide();
if (el.matches("canvas.pv")) {
el.removeAttribute("title");
return tipHide();
}
if (el.hasAttribute("title")) {
const t = el.getAttribute("title");
el.removeAttribute("title");
if (t) el.dataset.tip = t;
}
const text = el.dataset.tip;
if (!text) return tipHide();
if (el === tipEl) return;
tipEl = el;
const b = el.getBoundingClientRect();
tipFollow = b.width > 320 || b.height > 220;
tipText.textContent = text;
tipPlace(e);
tip.classList.add("on");
});
root.addEventListener("mousemove", (e) => {
if (tipEl && tipFollow) tipPlace(e);
});
root.addEventListener("mouseleave", tipHide);
for (const ev of ["pointerdown", "wheel", "click"]) root.addEventListener(ev, tipHide, true);
window.addEventListener("blur", tipHide);
const setVisible = (v) => {
visible = v;
wrap.style.display = v ? "flex" : "none";
if (v) firstRender();
};
const NAV_MARK = "data-checkpoint-preview-btn";
const NAV_SVG = '';
let navBtn = null, navQueued = false;
const navStyle = document.createElement("style");
navStyle.setAttribute(NAV_MARK + "-style", "");
navStyle.textContent = `
@keyframes cp-nav-wiggle{
0%{transform:rotate(0) scale(1)}
25%{transform:rotate(-14deg) scale(1.12)}
55%{transform:rotate(10deg) scale(1.12)}
80%{transform:rotate(-5deg) scale(1.05)}
100%{transform:rotate(0) scale(1)}
}
[${NAV_MARK}]:hover svg path{
transform-box:fill-box !important;transform-origin:center !important;
animation:cp-nav-wiggle .48s cubic-bezier(.3,.7,.4,1) 1 !important}`;
document.head.appendChild(navStyle);
const isButtonBox = (el) => !!el && el.nodeType === 1 && !el.closest('[role="dialog"]')
&& el.querySelectorAll(':scope > button:not([' + NAV_MARK + '])').length >= 1
&& el.querySelectorAll('button[role="switch"]').length >= 2;
function findPanelButtons() {
const acct = document.querySelector('[class*="accountPopoutButtonWrapper"]');
if (acct?.parentElement) {
const box = [...acct.parentElement.children].find(isButtonBox);
if (box) return box;
}
for (const sw of document.querySelectorAll('button[role="switch"]')) {
let el = sw.parentElement;
for (let i = 0; el && i < 4; i++, el = el.parentElement) if (isButtonBox(el)) return el;
}
return null;
}
function ensureNavButton() {
if (destroyed) return;
if (navBtn?.isConnected) return;
for (const old of document.querySelectorAll("[" + NAV_MARK + "]")) old.remove();
const box = findPanelButtons();
if (!box) return;
const tpl = [...box.children].filter((c) => c.tagName === "BUTTON" && !c.hasAttribute(NAV_MARK)).pop();
if (!tpl) return;
const b = tpl.cloneNode(true);
b.removeAttribute("data-migration-pending");
b.removeAttribute("id");
b.removeAttribute("aria-expanded");
b.removeAttribute("aria-checked");
b.removeAttribute("role");
b.setAttribute(NAV_MARK, "");
b.setAttribute("type", "button");
b.setAttribute("aria-label", "Checkpoint Preview");
b.removeAttribute("title");
b.addEventListener("mouseenter", () => {
tipEl = b;
tipFollow = false;
tipText.textContent = "Checkpoint Preview";
tipPlace({});
tip.classList.add("on");
});
b.addEventListener("mouseleave", () => {
if (tipEl === b) tipHide();
});
const inner = b.querySelector('[class*="contents"]') || b;
const holder = inner.querySelector('[class*="lottieIcon"]') || inner.firstElementChild;
if (holder) {
holder.innerHTML = NAV_SVG;
holder.style.display = "flex";
} else {
inner.innerHTML = '' + NAV_SVG + "
";
}
b.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
tipHide();
setVisible(!visible);
});
box.insertBefore(b, tpl);
navBtn = b;
}
const queueNav = () => {
if (navQueued || destroyed || navBtn?.isConnected) return;
navQueued = true;
requestAnimationFrame(() => {
navQueued = false;
ensureNavButton();
});
};
const navObserver = new MutationObserver(queueNav);
navObserver.observe(document.body, {childList: true, subtree: true});
const navTimer = setInterval(ensureNavButton, 3000);
ensureNavButton();
const thumbCache = new Map();
async function thumb(c, id) {
const key = c + ":" + id;
let cv = thumbCache.get(key);
if (cv) return cv;
await yieldUI();
if (destroyed) return null;
const src = srcData(c, id);
cv = document.createElement("canvas");
cv.width = src.w;
cv.height = src.h;
const displayScale = Math.min(1, 120 / src.w, 120 / src.h);
cv.style.cssText = `width:${src.w * displayScale}px;height:${src.h * displayScale}px`;
cv.title = `${src.w} × ${src.h} px`;
cv.getContext("2d").drawImage(src.cv, 0, 0);
thumbCache.set(key, cv);
return cv;
}
let gridSeq = 0;
async function renderGrid() {
syncAdj();
const my = ++gridSeq, c = S.cat;
root.querySelectorAll(".tab").forEach((b) => b.classList.toggle("on", b.dataset.cat === c));
moveInd();
const anim = animateNext;
let n = 0;
grid.textContent = "";
const add = (id, node, label) => {
const b = document.createElement("button");
b.className = "it" + (sel[c] === id ? " on" : "") + (id == null ? " none" : "");
b.dataset.id = id ?? "";
const s = document.createElement("small");
s.textContent = label;
b.append(node, s);
if (anim) {
b.classList.add("enter");
b.style.animationDelay = Math.min(n++, 14) * 22 + "ms";
}
grid.append(b);
return b;
};
if (OPTIONAL.has(c)) {
const x = document.createElement("b");
x.textContent = "×";
add(null, x, "None");
}
const items = D[c].map((_, i) => add(i, document.createElement("span"), String(i + 1).padStart(2, "0")));
items.find((b) => b.classList.contains("on"))?.scrollIntoView({block: "nearest"});
for (let i = 0; i < items.length; i++) {
if (destroyed || my !== gridSeq) return;
const cv = await thumb(c, i);
if (!cv || destroyed || my !== gridSeq) return;
items[i].firstChild.replaceWith(cv);
}
}
let seq = 0;
async function refresh() {
const my = ++seq;
busy.classList.add("on");
try {
const cv = await compose(sel, {bg: bgSpec(), isStale: () => my !== seq});
if (!cv || my !== seq) return;
pctx.clearRect(0, 0, PREVIEW, PREVIEW);
pctx.drawImage(cv, 0, 0);
} finally {
if (my === seq) busy.classList.remove("on");
}
}
async function exportBlob() {
busy.textContent = `Rendering ${PREVIEW}px…`;
busy.classList.add("on");
try {
const cv = await compose(sel, {bg: bgSpec()});
if (!cv) throw new Error("Preview closed");
return await cv.convertToBlob({type: "image/png"});
} finally {
busy.textContent = "Rendering…";
busy.classList.remove("on");
}
}
const fileName = () => `checkpoint-avatar-${PREVIEW}.png`;
console.log("%cCheckpoint Preview Reworked " + CREDIT + " · " + CREDIT_URL, "font-weight:bold;color:#5865f2");
async function waitForAttachment(timeout = 6000) {
const sel = '[class*="attachedFiles"],[class*="channelAttachmentArea"],[class*="attachmentArea"]';
const t0 = Date.now();
while (Date.now() - t0 < timeout) {
if (destroyed) return false;
if (document.querySelector(sel)) {
await new Promise((r) => setTimeout(r, 450));
return true;
}
await new Promise((r) => setTimeout(r, 100));
}
return false;
}
function pressEnter(box) {
const init = {key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true};
box.focus();
box.dispatchEvent(new KeyboardEvent("keydown", init));
box.dispatchEvent(new KeyboardEvent("keypress", init));
box.dispatchEvent(new KeyboardEvent("keyup", init));
}
const CREDIT_ID = CREDIT_URL.split("/").pop();
function discordRequire() {
try {
const chunk = W.webpackChunkdiscord_app;
if (!chunk) return null;
let req = null;
chunk.push([[Symbol("checkpoint-preview")], {}, (r) => { req = r; }]);
chunk.pop();
return req;
} catch {
return null;
}
}
function findDiscordFn(name) {
const req = discordRequire();
if (!req?.c) return null;
for (const id in req.c) {
const ex = req.c[id]?.exports;
if (!ex || ex === window || typeof ex !== "object" && typeof ex !== "function") continue;
try {
if (typeof ex[name] === "function") return ex[name].bind(ex);
for (const k in ex) {
const v = ex[k];
if (v && typeof v === "object" && typeof v[name] === "function") return v[name].bind(v);
}
} catch {
}
}
return null;
}
async function openCreditProfile() {
const id = CREDIT_ID;
setVisible(false);
try {
const getUser = W.Vencord?.Webpack?.Common?.UserUtils?.getUser;
if (getUser) await getUser(id).catch(() => {
});
} catch {
}
try {
const open = findDiscordFn("openUserProfileModal");
if (open) {
open({userId: id, analyticsLocation: {page: "Guild Channel", section: "Profile Popout"}});
return;
}
} catch (e) {
console.warn("[Checkpoint Preview Reworked] openUserProfileModal failed:", e);
}
try {
window.history.pushState(window.history.state, "", "/users/" + id);
window.dispatchEvent(new PopStateEvent("popstate", {state: window.history.state}));
} catch (e) {
console.warn("[Checkpoint Preview Reworked] Router fallback failed:", e);
setVisible(true);
toast("Couldn't open the profile here");
}
}
async function act(a) {
if (a === "close") return setVisible(false);
if (a === "open") return setVisible(true);
if (a === "undo") return undo();
if (a === "rand") {
pushHistory();
randomize();
renderGrid();
return refresh();
}
if (a === "reset") {
pushHistory();
Object.assign(sel, DEFAULT);
for (const k in ADJ) delete ADJ[k];
Object.assign(ADJ, JSON.parse(JSON.stringify(DEFAULT_ADJ)));
renderGrid();
return refresh();
}
if (a === "copy") {
const txt = presetJSON();
if (await copyText(txt)) toast("Preset copied: share it with anyone ✔");
else openDlg("export", txt);
return;
}
if (a === "import") return openDlg("import");
if (a === "dlgclose") return closeDlg();
if (a === "gswap") {
[S.gradA, S.gradB] = [S.gradB, S.gradA];
S.bg = "customgrad";
syncGrad();
updateDD();
return refresh();
}
if (a === "dlgpaste") {
try {
const txt = await navigator.clipboard.readText();
if (!txt) return toast("Clipboard is empty");
dlgText.value = txt.trim();
dlgText.focus();
} catch {
toast("Clipboard blocked here: click the box and press Ctrl+V");
}
return;
}
if (a === "dlgok") {
if (dlgMode === "gradient") {
gradSnap = null;
return closeDlg();
}
if (dlgMode === "import") {
try {
applyPreset(dlgText.value);
closeDlg();
toast("Preset imported ✔");
} catch (e) {
toast(e.message || "Couldn't import preset");
}
} else if (dlgMode === "export") {
toast((await copyText(dlgText.value)) ? "Preset copied ✔" : "Press Ctrl+C to copy");
}
return;
}
try {
const blob = await exportBlob();
if (a === "save") {
const url = URL.createObjectURL(blob), l = document.createElement("a");
l.href = url;
l.download = fileName();
root.append(l);
l.click();
l.remove();
setTimeout(() => URL.revokeObjectURL(url), 4000);
toast("PNG downloaded ✔");
} else if (a === "copyimg") {
await navigator.clipboard.write([new ClipboardItem({"image/png": blob})]);
toast("Image copied: paste it anywhere with Ctrl+V ✔");
} else if (a === "attach") {
const box = document.querySelector('div[role="textbox"][contenteditable="true"]');
if (!box) {
await navigator.clipboard.write([new ClipboardItem({"image/png": blob})]);
return toast("Open a chat and paste with Ctrl+V (image already copied)");
}
setVisible(false);
box.focus();
const dt = new DataTransfer();
dt.items.add(new File([blob], fileName(), {type: "image/png"}));
box.dispatchEvent(new ClipboardEvent("paste", {clipboardData: dt, bubbles: true, cancelable: true}));
} else if (a === "send") {
const box = document.querySelector('div[role="textbox"][contenteditable="true"]');
if (!box) {
await navigator.clipboard.write([new ClipboardItem({"image/png": blob})]);
return toast("Open a chat first (image already copied)");
}
setVisible(false);
box.focus();
const dt = new DataTransfer();
dt.items.add(new File([blob], fileName(), {type: "image/png"}));
box.dispatchEvent(new ClipboardEvent("paste", {clipboardData: dt, bubbles: true, cancelable: true}));
if (await waitForAttachment()) {
pressEnter(box);
toast("Sent to chat ✔");
} else {
toast("Attached. Press Enter to send");
}
}
} catch (e) {
console.error(e);
toast("Failed: " + (e.message || e));
}
}
const adjKey = () => (sel[S.cat] == null ? null : S.cat + ":" + sel[S.cat]);
const fmtAdj = {
x: (v) => v.toFixed(1), y: (v) => v.toFixed(1), s: (v) => v.toFixed(2) + "×", r: (v) => Math.round(v) + "°"
};
function syncAdj() {
const box = $(".adj"), k = adjKey(), a = k ? ADJ[k] || NOADJ : NOADJ;
box.classList.toggle("off", !k);
$(".an").textContent = k ? `${LABEL[S.cat]} ${String(sel[S.cat] + 1).padStart(2, "0")}` : "select an item";
box.querySelectorAll("input[data-j]").forEach((i) => {
i.value = a[i.dataset.j];
i.nextElementSibling.textContent = fmtAdj[i.dataset.j](a[i.dataset.j]);
const mn = +i.min, mx = +i.max, pc = (v) => `calc(${(v - mn) / (mx - mn)} * (100% - 14px) + 7px)`;
i.style.setProperty("--lo", pc(Math.min(+i.value, 0)));
i.style.setProperty("--hi", pc(Math.max(+i.value, 0)));
});
}
function setAdj(prop, val) {
const k = adjKey();
if (!k || !["x", "y", "r"].includes(prop) || !Number.isFinite(val)) return;
pushNudge();
const a = ADJ[k] || (ADJ[k] = {...NOADJ});
a[prop] = val;
syncAdj();
refresh();
}
const adjJSON = () => JSON.stringify(Object.fromEntries(Object.entries(ADJ)
.filter(([, a]) => a.x || a.y || a.s !== 1 || a.r)
.map(([k, a]) => [k, {x: +a.x.toFixed(2), y: +a.y.toFixed(2), s: +a.s.toFixed(3), r: +a.r.toFixed(1)}])));
const undoStack = [];
const snapshot = () => ({sel: {...sel}, adj: JSON.parse(JSON.stringify(ADJ))});
let lastNudge = 0;
function pushHistory() {
const snap = snapshot(), last = undoStack[undoStack.length - 1];
if (last && JSON.stringify(last) === JSON.stringify(snap)) return;
undoStack.push(snap);
if (undoStack.length > 100) undoStack.shift();
}
function pushNudge() {
const now = Date.now();
if (now - lastNudge > 700) pushHistory();
lastNudge = now;
}
function undo() {
const h = undoStack.pop();
if (!h) return toast("Nothing to undo");
Object.assign(sel, h.sel);
for (const k in ADJ) delete ADJ[k];
Object.assign(ADJ, h.adj);
lastNudge = 0;
renderGrid();
refresh();
toast("Undone ↶");
}
const presetJSON = () => JSON.stringify({
checkpointPreset: 1,
selection: {...sel},
adjustments: JSON.parse(adjJSON()),
...(S.bg === "image" ? {} : {background: S.bg}),
...(S.bg === "custom" ? {backgroundColor: S.bgColor} : {}),
...(S.bg === "customgrad" ? {backgroundGradient: {angle: S.gradAngle, colors: [S.gradA, S.gradB]}} : {})
});
function parsePreset(text) {
let o;
try {
o = JSON.parse(String(text).trim());
} catch {
throw new Error("That isn't valid JSON");
}
if (!o || typeof o !== "object" || Array.isArray(o)) throw new Error("Not a Checkpoint preset");
const legacy = !("selection" in o) && !("checkpointPreset" in o);
if (legacy && !Object.keys(o).length) throw new Error("Not a Checkpoint preset");
const rawSel = legacy ? null : o.selection, rawAdj = legacy ? o : (o.adjustments || {});
const nextSel = {...sel};
if (rawSel) {
if (typeof rawSel !== "object") throw new Error("Preset outfit is invalid");
for (const c of CATS) {
if (!(c in rawSel)) continue;
const v = rawSel[c];
if (v == null) {
if (!OPTIONAL.has(c)) throw new Error(`${LABEL[c]} can't be empty`);
nextSel[c] = null;
} else if (Number.isInteger(v) && v >= 0 && v < D[c].length) nextSel[c] = v;
else throw new Error(`${LABEL[c]} ${v} doesn't exist`);
}
}
const nextAdj = {}, num = (v, lo, hi) => (Number.isFinite(v) ? clamp(v, lo, hi) : 0);
for (const [k, v] of Object.entries(rawAdj)) {
const m = /^([a-z]+):(\d+)$/.exec(k);
if (!m || !CATS.includes(m[1]) || +m[2] >= D[m[1]].length || !v || typeof v !== "object") continue;
nextAdj[k] = {x: num(v.x, -40, 40), y: num(v.y, -40, 40), s: 1, r: num(v.r, -180, 180)};
}
if (legacy && !Object.keys(nextAdj).length) throw new Error("Not a Checkpoint preset");
const bg = typeof o.background === "string" && Object.hasOwn(BGS, o.background) ? o.background : null;
const color = typeof o.backgroundColor === "string" && /^#[0-9a-f]{6}$/i.test(o.backgroundColor) ? o.backgroundColor : null;
const hex = (v) => typeof v === "string" && /^#[0-9a-f]{6}$/i.test(v);
const bgg = o.backgroundGradient;
const grad = bgg && Array.isArray(bgg.colors) && bgg.colors.length >= 2 && hex(bgg.colors[0]) && hex(bgg.colors[1]) && Number.isFinite(bgg.angle)
? {angle: clamp(bgg.angle, 0, 360), a: bgg.colors[0], b: bgg.colors[1]} : null;
return {sel: nextSel, adj: nextAdj, bg: (bg === "custom" && !color) || (bg === "customgrad" && !grad) ? null : bg, color, grad};
}
function applyPreset(text) {
const p = parsePreset(text);
pushHistory();
Object.assign(sel, p.sel);
for (const k in ADJ) delete ADJ[k];
Object.assign(ADJ, p.adj);
if (p.bg) {
if (p.color) S.bgColor = p.color;
if (p.grad) {
S.gradA = p.grad.a;
S.gradB = p.grad.b;
S.gradAngle = p.grad.angle;
}
S.bg = p.bg;
updateDD();
}
renderGrid();
refresh();
}
async function copyText(txt) {
try {
await navigator.clipboard.writeText(txt);
return true;
} catch {
}
try {
const ta = document.createElement("textarea");
ta.value = txt;
ta.style.cssText = "position:fixed;opacity:0";
root.append(ta);
ta.select();
const ok = document.execCommand("copy");
ta.remove();
return ok;
} catch {
return false;
}
}
const dlg = $(".dlg"), dlgText = dlg.querySelector("textarea"), dlgOk = dlg.querySelector('[data-a="dlgok"]');
let dlgMode = null, gradSnap = null;
const gedit = $(".gedit"), dlgPaste = dlg.querySelector('[data-a="dlgpaste"]');
const gradCssNow = () => `linear-gradient(${S.gradAngle}deg,${S.gradA},${S.gradB})`;
function syncGrad() {
gedit.querySelector('[data-g="a"]').value = S.gradA;
gedit.querySelector('[data-g="b"]').value = S.gradB;
gedit.querySelector('[data-g="angle"]').value = S.gradAngle;
gedit.querySelector("output").textContent = S.gradAngle + "\u00b0";
gedit.querySelector(".gprev").style.background = gradCssNow();
}
function openDlg(mode, text = "") {
dlgMode = mode;
const imp = mode === "import", gr = mode === "gradient";
gradSnap = gr ? {bg: S.bg, a: S.gradA, b: S.gradB, angle: S.gradAngle} : null;
gedit.hidden = !gr;
dlgText.hidden = gr;
dlgPaste.hidden = !imp;
if (gr) {
S.bg = "customgrad";
syncGrad();
updateDD();
refresh();
}
$(".dt").textContent = gr ? "Custom gradient" : imp ? "Import preset" : "Share preset";
$(".dd").textContent = gr ? "Pick two colors and an angle. The preview updates live; Cancel restores your previous background." : imp ? "Paste the preset JSON someone shared with you. It replaces your current outfit and adjustments (you can undo it)."
: "Clipboard access is blocked here. Copy this JSON (Ctrl+C) and send it to anyone.";
dlgText.value = text;
dlgText.readOnly = !imp;
dlgOk.textContent = gr ? "Done" : imp ? "Import" : "Copy";
dlg.hidden = false;
if (gr) return;
dlgText.focus();
if (!imp) dlgText.select();
}
function closeDlg() {
if (dlgMode === "gradient" && gradSnap) {
S.bg = gradSnap.bg;
S.gradA = gradSnap.a;
S.gradB = gradSnap.b;
S.gradAngle = gradSnap.angle;
updateDD();
refresh();
}
gradSnap = null;
dlg.hidden = true;
dlgText.hidden = false;
gedit.hidden = true;
dlgMode = null;
}
const bgDD = $(".bgd"), bgBtn = $(".bgbtn"), bgMenu = $(".bgmenu"), bgColorIn = $(".bgcolor"), bgFile = $(".bgfile");
const bgCss = (k) => {
const v = BGS[k];
return typeof v === "string" ? v : v && v.grad ? `linear-gradient(${v.angle}deg,${v.grad.join(",")})` : "";
};
const sw = (css, chk) => ``;
const curSw = () => S.bg === "transparent" ? sw("", true)
: S.bg === "custom" ? sw(S.bgColor)
: S.bg === "customgrad" ? sw(gradCssNow())
: S.bg === "image" ? sw(`url(${S.bgThumb}) center/cover`)
: sw(bgCss(S.bg));
function updateDD() {
bgBtn.innerHTML = `${curSw()}${BG_LABEL[S.bg]}`;
const st = bgMenu.scrollTop;
const opt = (k, label, swatch) => ``;
let h = "";
for (const [g, items] of BG_MENU) h += `${g}
` + items.map(([k, l]) => opt(k, l, sw(bgCss(k), k === "transparent"))).join("");
h += `Custom
` + opt("custom", "Custom color\u2026", sw(S.bgColor));
h += opt("customgrad", "Custom gradient\u2026", sw(gradCssNow()));
h += ``;
if (S.bgImg) {
h += opt("image", "Current image", sw(`url(${S.bgThumb}) center/cover`));
h += `` + ["cover", "contain", "stretch"].map((f) => ``).join("") + `
`;
}
bgMenu.innerHTML = h;
bgMenu.scrollTop = st;
}
function closeDD() {
bgDD.classList.remove("open");
bgBtn.setAttribute("aria-expanded", "false");
}
function toggleDD() {
const open = !bgDD.classList.contains("open");
bgDD.classList.toggle("open", open);
bgBtn.setAttribute("aria-expanded", String(open));
if (open) bgMenu.querySelector(".bgo.on")?.scrollIntoView({block: "nearest"});
}
function setBgImage(bmp) {
S.bgImg?.close?.();
S.bgImg = bmp;
const t = document.createElement("canvas");
t.width = t.height = 48;
paintBackground(t.getContext("2d"), 48, {img: bmp, fit: "cover"});
S.bgThumb = t.toDataURL();
S.bg = "image";
updateDD();
refresh();
}
async function loadBgFile(f) {
try {
if (!f || !f.type.startsWith("image/")) throw new Error("That isn't an image file");
setBgImage(await createImageBitmap(f));
toast("Background set \u2714");
} catch (e) {
toast(e.message || "Couldn't read that image");
}
}
bgFile.addEventListener("change", () => {
const f = bgFile.files[0];
bgFile.value = "";
loadBgFile(f);
});
view.addEventListener("dragover", (e) => {
if ([...(e.dataTransfer?.types || [])].includes("Files")) {
e.preventDefault();
view.classList.add("drop");
}
});
view.addEventListener("dragleave", () => view.classList.remove("drop"));
view.addEventListener("drop", (e) => {
view.classList.remove("drop");
const f = e.dataTransfer?.files?.[0];
if (!f || !f.type.startsWith("image/")) return;
e.preventDefault();
loadBgFile(f);
});
updateDD();
let drag = null;
pv.addEventListener("pointerdown", (e) => {
if (!adjKey() || e.button > 0) return;
drag = {x: e.clientX, y: e.clientY};
pv.setPointerCapture(e.pointerId);
pv.classList.add("drag");
});
pv.addEventListener("pointermove", (e) => {
if (!drag) return;
const k = adjKey(), u = VIEW.s / pv.getBoundingClientRect().width;
if (!k) return;
if (!drag.pushed) {
pushHistory();
drag.pushed = true;
}
const a = ADJ[k] || (ADJ[k] = {...NOADJ});
a.x = clamp(a.x + (e.clientX - drag.x) * u, -40, 40);
a.y = clamp(a.y + (e.clientY - drag.y) * u, -40, 40);
drag.x = e.clientX;
drag.y = e.clientY;
syncAdj();
refresh();
});
const endDrag = () => {
drag = null;
pv.classList.remove("drag");
};
pv.addEventListener("pointerup", endDrag);
pv.addEventListener("pointercancel", endDrag);
$(".tabs").innerHTML = `` + CATS.map((c) => ``).join("");
const tabInd = $(".tab-ind");
const moveInd = (instant) => {
const on = root.querySelector(".tab.on");
if (!on || !on.offsetWidth) return;
tabInd.classList.toggle("nt", !!instant);
tabInd.style.width = on.offsetWidth + "px";
tabInd.style.height = on.offsetHeight + "px";
tabInd.style.transform = `translate(${on.offsetLeft}px,${on.offsetTop}px)`;
if (instant) {
void tabInd.offsetWidth;
tabInd.classList.remove("nt");
}
};
const tabsObserver = new ResizeObserver(() => moveInd(true));
tabsObserver.observe($(".tabs"));
let tabSwitchT = 0, tabSwitchSeq = 0, animateNext = false;
const switchTab = (next) => {
if (next === S.cat) return;
const dir = CATS.indexOf(next) > CATS.indexOf(S.cat) ? 1 : -1;
S.cat = next;
const my = ++tabSwitchSeq;
grid.style.setProperty("--dir", dir);
root.querySelectorAll(".tab").forEach((b) => b.classList.toggle("on", b.dataset.cat === next));
moveInd();
grid.classList.add("out");
clearTimeout(tabSwitchT);
tabSwitchT = setTimeout(() => {
if (destroyed || my !== tabSwitchSeq) return;
animateNext = true;
grid.scrollTop = 0;
renderGrid();
animateNext = false;
grid.style.transition = "none";
grid.classList.remove("out");
void grid.offsetWidth;
grid.style.transition = "";
}, 140);
};
root.addEventListener("click", (e) => {
const t = e.target;
if (t === dlg) return closeDlg();
if (t === wrap) return setVisible(false);
if (!t.closest(".bgd")) closeDD();
if (t.closest(".bgbtn")) return toggleDD();
const bo = t.closest(".bgo");
if (bo) {
const act2 = bo.dataset.bgact;
closeDD();
if (act2 === "file") return bgFile.click();
if (bo.dataset.bg === "customgrad") return openDlg("gradient");
S.bg = bo.dataset.bg;
updateDD();
refresh();
if (S.bg === "custom") {
bgColorIn.value = S.bgColor;
bgColorIn.click();
}
return;
}
const fb = t.closest("[data-fit]");
if (fb) {
S.bgFit = fb.dataset.fit;
updateDD();
return refresh();
}
const credit = t.closest("a[data-t]");
if (credit && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
return openCreditProfile();
}
const btn = t.closest("[data-a]");
if (btn) return act(btn.dataset.a);
const tab = t.closest(".tab");
if (tab) {
return switchTab(tab.dataset.cat);
}
const it = t.closest(".it");
if (it) {
const v = it.dataset.id;
pushHistory();
sel[S.cat] = v === "" ? null : +v;
grid.querySelectorAll(".it").forEach((b) => b.classList.toggle("on", b === it));
syncAdj();
refresh();
}
});
root.addEventListener("input", (e) => {
const j = e.target.dataset?.j;
if (j) {
setAdj(j, +e.target.value);
return;
}
const g = e.target.dataset?.g;
if (g) {
if (g === "angle") S.gradAngle = +e.target.value;
else if (g === "a") S.gradA = e.target.value;
else S.gradB = e.target.value;
S.bg = "customgrad";
syncGrad();
updateDD();
refresh();
return;
}
if (e.target === bgColorIn) {
S.bgColor = e.target.value;
S.bg = "custom";
updateDD();
refresh();
}
});
for (const ev of ["keydown", "keyup", "keypress"]) root.addEventListener(ev, (e) => e.stopPropagation());
const onKey = (e) => {
if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey && e.code === "KeyS") {
e.preventDefault();
e.stopImmediatePropagation();
setVisible(!visible);
} else if (e.key === "Escape" && visible) {
e.stopPropagation();
if (!dlg.hidden) closeDlg(); else if (bgDD.classList.contains("open")) closeDD(); else setVisible(false);
} else if (visible && dlg.hidden && (e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.code === "KeyZ") {
e.preventDefault();
e.stopImmediatePropagation();
undo();
}
};
window.addEventListener("keydown", onKey, true);
const onPaste = (e) => {
if (dlg.hidden || dlgText.hidden || dlgText.readOnly || !e.composedPath().includes(dlgText)) return;
const txt = e.clipboardData?.getData("text/plain");
if (txt == null) return;
e.preventDefault();
e.stopImmediatePropagation();
dlgText.setRangeText(txt, dlgText.selectionStart, dlgText.selectionEnd, "end");
};
window.addEventListener("paste", onPaste, true);
function destroy() {
if (destroyed) return;
destroyed = true;
seq++;
gridSeq++;
clearTimeout(toastT);
previewObserver.disconnect();
tabsObserver.disconnect();
themeObserver.disconnect();
navObserver.disconnect();
clearInterval(navTimer);
for (const n of document.querySelectorAll("[" + NAV_MARK + "]")) n.remove();
navStyle.remove();
clearTimeout(tabSwitchT);
scan.width = scan.height = 1;
window.removeEventListener("keydown", onKey, true);
window.removeEventListener("paste", onPaste, true);
host.remove();
thumbCache.clear();
srcCache.clear();
atlas.close?.();
S.bgImg?.close?.();
delete window[KEY];
}
window[KEY] = {
destroy, toggle: () => setVisible(!visible), selection: () => ({...sel}), set: (o) => {
pushHistory();
Object.assign(sel, o);
renderGrid();
return refresh();
}, export: exportBlob, adjust: () => JSON.parse(adjJSON()), undo, preset: presetJSON, importPreset: applyPreset, setAdjust: (o) => {
pushHistory();
for (const k in ADJ) delete ADJ[k];
for (const [k, v] of Object.entries(o || {})) ADJ[k] = {
x: Number.isFinite(v?.x) ? clamp(v.x, -40, 40) : 0,
y: Number.isFinite(v?.y) ? clamp(v.y, -40, 40) : 0,
s: 1,
r: Number.isFinite(v?.r) ? clamp(v.r, -180, 180) : 0
};
syncAdj();
return refresh();
}
};
const firstRender = () => {
if (rendered) return;
rendered = true;
renderGrid();
refresh();
};
let rendered = false;
wrap.style.display = visible ? "flex" : "none";
if (visible) firstRender();
return true;
};
let starting = true;
const start = async (interactive) => {
try {
return !!(await run(interactive));
} catch (e) {
console.error("[Checkpoint Preview Reworked]", e);
return false;
}
};
(async () => {
for (const wait of [0, 5000, 15000]) {
if (wait) await new Promise((r) => setTimeout(r, wait));
if (window[KEY]) break;
if (await start(false)) break;
}
starting = false;
})();
window.addEventListener("keydown", (e) => {
if (starting || window[KEY] || !(e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey && e.code === "KeyS")) return;
e.preventDefault();
e.stopImmediatePropagation();
starting = true;
start(true).finally(() => {
starting = false;
});
}, true);
})();