Skip to content

Commit d894823

Browse files
committed
Add record function through option -t
Resolve #4
1 parent ff1a4dd commit d894823

6 files changed

Lines changed: 220 additions & 147 deletions

File tree

bin/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ program
4545
.option('-o, --output <output>', 'Custom filename of the generated image')
4646
.option('-x, --scale <scale>', 'Scale factor of the generated image, defaults to `2`')
4747
.option('-s, --selector <selector>', 'CSS selector to target the rendered node, defaults to `css-doodle`')
48-
.option('-d, --delay <delay>', 'Delay in milliseconds after the image is rendered')
48+
.option('-d, --delay <delay>', 'Delay after the image is rendered, e.g, `2s`')
49+
.option('-t, --time <time>', 'Record screen for a specific time, e.g, `10s')
4950
.action(handleRender);
5051

5152
program

lib/handler.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,32 @@ import { config, configPath, configDownloadPath } from './static.js'
66
import { generateSVG, generateShape } from './generate.js';
77
import { parse } from './parse.js';
88
import { preview } from './preview/index.js';
9-
import { render } from './render.js';
9+
import { render } from './render/index.js';
1010
import { read } from './read.js';
1111

1212
export async function handleRender(source, options) {
1313
const { content, error, type } = await read(source);
1414
if (error) {
1515
return console.error(error.message);
1616
}
17-
let title = 'image';
17+
options.type = type;
18+
options.delay = readTime(options.delay, { max: 30 * 1000 }) || 0;
19+
options.time = readTime(options.time, { max: 60 * 1000 }) || 0;
20+
options.scale = Number(options.scale) || 2;
21+
options.selector ??= 'css-doodle';
22+
23+
let title = options.time ? 'record' : 'image';
1824
if (source) {
1925
const basename = path.basename(source);
2026
const extname = path.extname(basename);
2127
title = extname ? basename.split(extname)[0] : basename;
2228
}
2329
options.title = title;
24-
options.type = type;
2530

2631
try {
2732
const start = Date.now();
2833
const output = await render(content, options);
2934
const time = (Date.now() - start) / 1000;
30-
3135
if (output) {
3236
console.log(`Saved to ${output}. (${time}s)`);
3337
}
@@ -182,3 +186,17 @@ async function fetchResource(url) {
182186
let res = await fetch(url, { redirect: 'follow' });
183187
return Buffer.from(await res.arrayBuffer()).toString();
184188
}
189+
190+
function readTime(number, options = {}) {
191+
let result = 0;
192+
if (/^(\d+)(ms)?$/.test(number)) {
193+
result = Number(number.replace('ms', ''));
194+
}
195+
if (/^(\d+)s$/.test(number)) {
196+
result = Number(number.replace('s','')) * 1000;
197+
}
198+
if (/^(\d+)m$/.test(number)) {
199+
result = Number(number.replace('m', '')) * 60 * 1000;
200+
}
201+
return Math.min(result, options.max ?? Infinity);
202+
}

lib/render.js

Lines changed: 0 additions & 142 deletions
This file was deleted.

lib/render/index.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
3+
import puppeteer from 'puppeteer';
4+
5+
import { getCssDoodleLib, getBrowserPath, defaultAppArgs } from '../static.js';
6+
import { screenshot } from './screenshot.js';
7+
import { screencast } from './screencast.js';
8+
9+
export async function render(code, options = {}) {
10+
const args = [
11+
...defaultAppArgs,
12+
'--start-maximized',
13+
];
14+
15+
if (options.type === 'codepen') {
16+
args.push('--ignore-certificate-errors');
17+
}
18+
19+
const settings = {
20+
defaultViewport: null,
21+
args,
22+
};
23+
24+
let browserPath = getBrowserPath();
25+
if (browserPath) {
26+
settings.executablePath = browserPath;
27+
}
28+
29+
const browser = await puppeteer.launch(settings);
30+
const page = await browser.newPage();
31+
32+
switch (options.type) {
33+
case 'css': case 'cssd': case 'stdin': {
34+
page.setContent(buildHTML(code, getCssDoodleLib()));
35+
break;
36+
}
37+
case 'html': {
38+
page.setContent(code);
39+
break;
40+
}
41+
case 'codepen': {
42+
await page.goto(code);
43+
const iframe = await page.$('iframe#result');
44+
if (iframe) {
45+
const code = await page.evaluate(el => el.getAttribute('srcdoc'), iframe);
46+
page.setContent(code, { waitUntil: 'networkidle0' });
47+
} else {
48+
throw new Error('eroor: read CodePen failed');
49+
}
50+
break;
51+
}
52+
default: {
53+
throw new Error('error: invalid type `${options.type}`');
54+
}
55+
}
56+
57+
if (!options.output) {
58+
let tag = Date.now();
59+
if (/^(css|cssd|stdin)$/.test(options.type)) {
60+
tag = await page.$eval('css-doodle', el => el.seed);
61+
}
62+
options.output = `${options.title || 'screenshot'}-${tag}.png`;
63+
}
64+
65+
if (options.delay) {
66+
await setTimeout(options.delay);
67+
}
68+
69+
const output = options.time
70+
? await screencast(page, options)
71+
: await screenshot(page, options);
72+
73+
await browser.close();
74+
return output;
75+
}
76+
77+
function buildHTML(code, cssDoodleLib) {
78+
return `<!doctype html>
79+
<html>
80+
<head>
81+
<meta charset="utf-8">
82+
<style>
83+
html, body, #container { margin: 0; padding: 0; width: 100%; height: 100% }
84+
body, #container { display: grid; place-items: center; }
85+
</style>
86+
<script>${cssDoodleLib}</script>
87+
</head>
88+
<body>
89+
<div id="container">
90+
<css-doodle><template>${code}</template></css-doodle>
91+
</div>
92+
</body>
93+
</html>`;
94+
}

lib/render/screencast.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
3+
const defaultWidth = 1600;
4+
const defaultHeight = 900;
5+
6+
export async function screencast(page, options) {
7+
let { scale, output, selector } = options;
8+
9+
await page.setViewport({
10+
width: defaultWidth,
11+
height: defaultHeight,
12+
deviceScaleFactor: scale
13+
});
14+
15+
const clip = await page.evaluate(selector => {
16+
const element = document.querySelector(selector);
17+
if (element) {
18+
const { width, height, x, y } = element.getBoundingClientRect();
19+
return {
20+
x, y,
21+
width,
22+
height: height || width,
23+
}
24+
} else {
25+
const doc = document.documentElement;
26+
return {
27+
x: 0, y: 0,
28+
width: doc.scrollWidth,
29+
height: doc.scrollHeight,
30+
}
31+
}
32+
}, selector);
33+
34+
await page.setViewport({
35+
width: Math.ceil(clip.width) || defaultWidth,
36+
height: Math.ceil(clip.height) || defaultHeight,
37+
deviceScaleFactor: scale
38+
});
39+
40+
output = options.output.replace(/\.png$/, '.webm');
41+
const recorder = await page.screencast({
42+
path: output,
43+
scale: scale,
44+
clip
45+
});
46+
47+
await setTimeout(options.time);
48+
await recorder.stop();
49+
return output;
50+
}

0 commit comments

Comments
 (0)