Skip to content

Instantly share code, notes, and snippets.

@killroy42
Last active February 22, 2023 18:25
Show Gist options
  • Save killroy42/5f78381f3994ae5f4e460fd31e7101f0 to your computer and use it in GitHub Desktop.
Save killroy42/5f78381f3994ae5f4e460fd31e7101f0 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name SudokuPad Screenshot & Replay Recorder
// @namespace https://svencodes.com/
// @version 0.6
// @downloadURL https://gist.github.com/killroy42/5f78381f3994ae5f4e460fd31e7101f0/raw/sudokupad-screenshot&replay.user.js
// @updateURL https://gist.github.com/killroy42/5f78381f3994ae5f4e460fd31e7101f0/raw/sudokupad-screenshot&replay.user.js
// @supportURL https://svencodes.com
// @description Puzzle screenshots and replay GIF recording for Sven's SudokuPad
// @author sven@svencodes.com
// @match https://*.sudokupad.app/*
// @match https://*.crackingthecryptic.com/*
// @match http://localhost:*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// ==/UserScript==
(async () => {
'use strict';
console.print = (...args) => queueMicrotask(console.log.bind(console, ...args));
const wait = async ms => new Promise(resolve => setTimeout(resolve, ms));
const requireScript = async src => new Promise((onload, onerror) => document.getElementsByTagName('head')[0].appendChild(Object.assign(document.createElement('script'), {src, onload, onerror})));
const createGif = async opts => new Promise((resolve, reject) => gifshot.createGIF(opts, ({error, image}) => error ? reject(error) : resolve(image)));
const asciiProgress = ((p,l=10,sym=['⚪️','🔵'])=>[...Array(l)].map((_,i)=>sym[(i<(p*l)|0)|0]).join(''));
const recordReplayFrames = async (puzzle, {elem, replay, frameCount, onFrame}) => {
let frames = [];
let replayTotalTime = Puzzle.replayLength({actions: replay.actions});
for(let f = 0; f < frameCount; f++) {
let frameTime = Math.round(replayTotalTime / (frameCount - 1) * f);
await puzzle.replayPlay(replay, {speed: -1, playToTime: frameTime});
let frame = await domtoimage.toSvg(elem, {bgcolor: 'white'});
frames.push(frame);
if(typeof onFrame === 'function') onFrame(frame);
}
return frames
};
const sanitizeInt = (val, min, max) => Math.max(min, Math.min(max, parseInt(val)));
const recordReplay = async () => {
let pRequire = Promise.all([
'https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js',
'https://cdn.jsdelivr.net/npm/gifshot@0.4.5/dist/gifshot.min.js',
].map(requireScript));
const {app, app: {puzzle}} = Framework;
let replayData = app.getReplay();
let replay = Replay.decode(replayData);
console.log('Replay:', replay);
let replayTotalTime = Puzzle.replayLength({actions: replay.actions});
let frameDefault = '10';
let framePrompt = prompt(`The replay is ${(replayTotalTime/1000).toFixed(1)}s long. How many frames do you want to record? (max 100)`, frameDefault);
console.log('framePrompt:', framePrompt);
let frameCount = sanitizeInt(framePrompt || frameDefault, 0, 100);
console.log('frameCount:', frameCount);
let resDefault = '256x256';
let resPrompt = prompt(`What resolution do you want for the output GIF?`, resDefault);
console.log('resPrompt:', resPrompt);
let [_, width, height] = (resPrompt || resDefault).match(/^(\d+)x(\d+)$/) || [];
width = sanitizeInt(width, 32, 1024);
height = sanitizeInt(height, 32, 1024);
console.log('width/height:', width, height);
let gifOpts = {
gifWidth: width, gifHeight: height,
frameDuration: 2,
numWorkers: 4,
};
await alert('Ready to start. This may take a while. Progress in the console.');
Framework.closeDialog();
await wait(10);
await pRequire;
console.log('Replay duration: %fs', Math.round(replayTotalTime / 1000));
console.log('Replay actions: %d', replay.actions.length);
let frameElem = document.querySelector('#svgrenderer');
let currentFrame = 0, frameSize = 0;
const handleFrameProgress = frame => {
currentFrame++;
let frameTime = Math.round(replayTotalTime / (frameCount - 1) * currentFrame);
frameSize += frame.length;
console.print('Recording frame: [%s] %s of %s (time: %f/%fs, size: %fmb)',
asciiProgress(currentFrame / frameCount, 10),
currentFrame, frameCount,
(frameTime / 1000).toFixed(1), (replayTotalTime / 1000).toFixed(1),
(frameSize / 1024 / 1024).toFixed(1)
);
};
const handleGifProgress = progress => console.print('Generating GIF: [%s] %f%', asciiProgress(progress, 10), (progress * 100).toFixed(1));
console.time('Time to record frames');
let frames = await recordReplayFrames(puzzle, {
elem: frameElem, replay, frameCount,
onFrame: handleFrameProgress
});
console.timeEnd('Time to record frames');
//console.log('frames:', frames);
console.info('Create GIF animation...');
//console.log('gifOpts:', Object.assign(gifOpts, {images: frames, progressCallback: handleGifProgress}));
console.time('Time to create GIF');
let gifImg = await createGif(Object.assign(gifOpts, {images: frames, progressCallback: handleGifProgress}));
console.timeEnd('Time to create GIF');
GM_openInTab(gifImg);
console.info('GIF completed.');
};
const saveScreenshot = async () => {
let pRequire = requireScript('https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js');
Framework.closeDialog();
await wait(10);
await pRequire;
let frameElem = document.querySelector('#svgrenderer');
let bgcolor = getComputedStyle(document.body).backgroundColor;
let img = await domtoimage.toPng(frameElem, {bgcolor});
GM_openInTab(img);
};
GM_registerMenuCommand('Record Replay', recordReplay, 'R');
GM_registerMenuCommand('Puzzle Screenshot', saveScreenshot, 'S');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment