Last active
February 22, 2023 18:25
-
-
Save killroy42/5f78381f3994ae5f4e460fd31e7101f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 [email protected] | |
// @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/[email protected]/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