fragment-tools 0.2.9 → 0.2.11
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/package.json +5 -6
- package/src/cli/createConfig.js +38 -19
- package/src/cli/plugins/hot-shader-replacement.js +0 -5
- package/src/cli/plugins/save.js +7 -2
- package/src/cli/run.js +27 -6
- package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +3 -1
- package/src/client/app/lib/canvas-recorder/GIFRecorder.js +11 -1
- package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +97 -0
- package/src/client/app/modules/Exports.svelte +28 -4
- package/src/client/app/state/exports.svelte.js +40 -2
- package/src/client/app/state/rendering.svelte.js +1 -1
- package/src/client/app/ui/Field.svelte +7 -3
- package/src/client/app/ui/FieldSection.svelte +1 -1
- package/src/client/app/ui/LayoutBuild.svelte +1 -1
- package/src/client/app/ui/SketchRenderer.svelte +5 -1
- package/src/client/app/ui/fields/CheckboxInput.svelte +9 -7
- package/src/client/app/ui/fields/ImageInput.svelte +33 -15
- package/src/client/app/ui/fields/Select.svelte +2 -1
- package/src/client/app/utils/canvas.utils.js +21 -19
- package/src/index.js +12 -0
- package/src/types/config.d.ts +6 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/props.d.ts +32 -5
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.prettierignore +0 -5
- package/.prettierrc +0 -25
- package/CHANGELOG.md +0 -10
- package/src/client/app/lib/canvas-recorder/MP4Recorder.js +0 -44
- package/src/client/app/lib/canvas-recorder/WebMRecorder.js +0 -29
- package/src/client/app/lib/canvas-recorder/mp4.js +0 -1654
- package/src/client/app/lib/canvas-recorder/mp4.wasm +0 -0
- package/src/client/public/preview.html +0 -59
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fragment-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "A web development environment for creative coding",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"fragment": "bin/index.js"
|
|
8
8
|
},
|
|
@@ -26,22 +26,21 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@clack/core": "^0.4.2",
|
|
28
28
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
29
|
-
"body-parser": "^2.2.0",
|
|
30
29
|
"changedpi": "^1.0.4",
|
|
31
30
|
"convert-length": "^1.0.1",
|
|
32
31
|
"get-port": "^7.1.0",
|
|
33
32
|
"gifenc": "^1.0.3",
|
|
34
|
-
"glslify": "^7.1.1",
|
|
35
33
|
"is-unicode-supported": "^2.0.0",
|
|
36
34
|
"kleur": "^4.1.4",
|
|
35
|
+
"mediabunny": "^1.13.3",
|
|
36
|
+
"milliparsec": "^5.1.0",
|
|
37
37
|
"sade": "^1.7.4",
|
|
38
38
|
"svelte": "^5.19.0",
|
|
39
39
|
"vite": "^6.3.5",
|
|
40
|
-
"webm-writer": "^1.0.0",
|
|
41
40
|
"ws": "^8.2.3"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
|
-
"@changesets/cli": "^2.29.
|
|
43
|
+
"@changesets/cli": "^2.29.7",
|
|
45
44
|
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
|
|
46
45
|
"@types/p5": "^1.7.6",
|
|
47
46
|
"@types/three": "^0.174.0",
|
package/src/cli/createConfig.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import url from 'node:url';
|
|
3
4
|
import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
|
|
4
5
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
5
6
|
|
|
@@ -8,30 +9,47 @@ import { __dirname, file } from './utils.js';
|
|
|
8
9
|
import { log } from './log.js';
|
|
9
10
|
import sketches from './plugins/sketches.js';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {{ cwd: string, filepath: string | undefined }} params
|
|
15
|
+
* @returns {Promise<import('../types/config.js').Config>}
|
|
16
|
+
*/
|
|
11
17
|
export async function loadConfig({ cwd, filepath }) {
|
|
12
18
|
try {
|
|
13
|
-
let filename = `fragment.config.js`;
|
|
14
|
-
let configFile = filepath ? filepath : filename;
|
|
15
19
|
let configRoot = cwd;
|
|
16
|
-
let
|
|
20
|
+
let filenames = [`fragment.config.js`, `fragment.config.ts`];
|
|
17
21
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
log.error(`Config file not found: ${resolvedPath}`);
|
|
21
|
-
}
|
|
22
|
-
return {};
|
|
22
|
+
if (filepath) {
|
|
23
|
+
filenames = [filepath, ...filenames];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
let filepaths = filenames.map((filename) => {
|
|
27
|
+
return path.resolve(configRoot, filename);
|
|
28
|
+
});
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
command: 'build',
|
|
30
|
-
mode: 'dev',
|
|
31
|
-
},
|
|
32
|
-
configFile,
|
|
33
|
-
configRoot,
|
|
30
|
+
let resolvedIndex = filepaths.findIndex((filepath) =>
|
|
31
|
+
fs.existsSync(filepath),
|
|
34
32
|
);
|
|
33
|
+
/** @type {string|undefined} */
|
|
34
|
+
let resolvedPath = filepaths[resolvedIndex];
|
|
35
|
+
|
|
36
|
+
if (filepath && resolvedIndex !== 0) {
|
|
37
|
+
log.error(`Config file not found: ${filepath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!resolvedPath) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let configFile = path.relative(cwd, resolvedPath);
|
|
45
|
+
|
|
46
|
+
log.info(`Extending configuration from ${configFile}`);
|
|
47
|
+
|
|
48
|
+
const config = (
|
|
49
|
+
await import(
|
|
50
|
+
`${url.pathToFileURL(configFile).href}?ts=${Date.now()}`
|
|
51
|
+
)
|
|
52
|
+
).default;
|
|
35
53
|
|
|
36
54
|
return config;
|
|
37
55
|
} catch (error) {
|
|
@@ -43,9 +61,10 @@ export async function loadConfig({ cwd, filepath }) {
|
|
|
43
61
|
/**
|
|
44
62
|
* Create Vite config from entries
|
|
45
63
|
* @param {string[]} entries
|
|
46
|
-
* @param {
|
|
64
|
+
* @param {object} [options]
|
|
47
65
|
* @param {boolean} [options.dev=false]
|
|
48
66
|
* @param {boolean} [options.build=false]
|
|
67
|
+
* @param {string} [configFilepath]
|
|
49
68
|
* @param {string} [cwd=process.cwd()]
|
|
50
69
|
* @returns {import('vite').UserConfig}
|
|
51
70
|
*/
|
|
@@ -63,8 +82,8 @@ export async function createConfig(
|
|
|
63
82
|
log.info(`Creating Vite configuration...`);
|
|
64
83
|
|
|
65
84
|
const config = await loadConfig({
|
|
66
|
-
filepath: configFilepath,
|
|
67
85
|
cwd,
|
|
86
|
+
filepath: configFilepath,
|
|
68
87
|
});
|
|
69
88
|
|
|
70
89
|
return mergeConfig(
|
|
@@ -110,7 +129,7 @@ export async function createConfig(
|
|
|
110
129
|
__DEV__: !build,
|
|
111
130
|
},
|
|
112
131
|
optimizeDeps: {
|
|
113
|
-
include: ['convert-length', '
|
|
132
|
+
include: ['convert-length', 'changedpi'],
|
|
114
133
|
},
|
|
115
134
|
}),
|
|
116
135
|
config.vite ?? {},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import glslify from 'glslify';
|
|
5
4
|
import { log, dim, green, yellow } from '../log.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -232,10 +231,6 @@ ${keyword}${shaderParts[1]}
|
|
|
232
231
|
shaderPath,
|
|
233
232
|
);
|
|
234
233
|
|
|
235
|
-
code = glslify(code, {
|
|
236
|
-
basedir: process.cwd(),
|
|
237
|
-
});
|
|
238
|
-
|
|
239
234
|
if (server) {
|
|
240
235
|
code = addShaderFilepath(code, shaderPath);
|
|
241
236
|
}
|
package/src/cli/plugins/save.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { writeFile } from 'node:fs/promises';
|
|
3
|
-
import
|
|
3
|
+
import { json } from 'milliparsec';
|
|
4
4
|
import { log, green, red } from '../log.js';
|
|
5
5
|
import { mkdirp } from '../utils.js';
|
|
6
6
|
|
|
@@ -50,7 +50,12 @@ export default function screenshot({
|
|
|
50
50
|
return {
|
|
51
51
|
name: 'save',
|
|
52
52
|
configureServer(server) {
|
|
53
|
-
|
|
53
|
+
const payloadLimit = 100 * 1024 * 1024; // 100mb
|
|
54
|
+
server.middlewares.use(
|
|
55
|
+
json({
|
|
56
|
+
payloadLimit,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
54
59
|
server.middlewares.use('/save', async (req, res, next) => {
|
|
55
60
|
if (req.method === 'POST') {
|
|
56
61
|
const { files } = req.body;
|
package/src/cli/run.js
CHANGED
|
@@ -26,17 +26,23 @@ import hotShaderReplacement from './plugins/hot-shader-replacement.js';
|
|
|
26
26
|
*/
|
|
27
27
|
export async function run(entry, options = {}) {
|
|
28
28
|
let fragmentServer;
|
|
29
|
+
/** @type {import('node:fs').FSWatcher} */
|
|
30
|
+
let watcher;
|
|
29
31
|
|
|
30
32
|
const cwd = process.cwd();
|
|
31
33
|
const command = `run`;
|
|
32
34
|
const prefix = log.prefix(command);
|
|
35
|
+
|
|
36
|
+
const stop = () => {
|
|
37
|
+
fragmentServer?.close();
|
|
38
|
+
watcher?.close();
|
|
39
|
+
};
|
|
40
|
+
|
|
33
41
|
const exit = () => {
|
|
34
42
|
process.off('SIGTERM', exit);
|
|
35
43
|
process.off('exit', exit);
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
fragmentServer.close();
|
|
39
|
-
}
|
|
45
|
+
stop();
|
|
40
46
|
|
|
41
47
|
console.log();
|
|
42
48
|
};
|
|
@@ -62,9 +68,10 @@ export async function run(entry, options = {}) {
|
|
|
62
68
|
);
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
const hasTSFiles = entries.some((entry) => entry.endsWith('ts'));
|
|
65
72
|
const tsConfigDirpath = path.join(cwd, FRAGMENT_DIRECTORY);
|
|
66
73
|
const tsConfigFilepath = path.join(tsConfigDirpath, 'tsconfig.json');
|
|
67
|
-
if (!fs.existsSync(tsConfigFilepath)) {
|
|
74
|
+
if (!fs.existsSync(tsConfigFilepath) && hasTSFiles) {
|
|
68
75
|
await createTsConfigFile(cwd);
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -106,6 +113,22 @@ export async function run(entry, options = {}) {
|
|
|
106
113
|
],
|
|
107
114
|
}),
|
|
108
115
|
);
|
|
116
|
+
|
|
117
|
+
watcher = fs.watch(cwd, (eventType, filename) => {
|
|
118
|
+
if (
|
|
119
|
+
['fragment.config.js', 'fragment.config.ts'].includes(
|
|
120
|
+
filename,
|
|
121
|
+
) ||
|
|
122
|
+
options.configFilepath?.includes(filename)
|
|
123
|
+
) {
|
|
124
|
+
log.warn(`${filename} has changed. Restarting...`);
|
|
125
|
+
console.log();
|
|
126
|
+
server.close();
|
|
127
|
+
stop();
|
|
128
|
+
run(entry, options);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
109
132
|
await server.listen();
|
|
110
133
|
|
|
111
134
|
// line break after logs
|
|
@@ -130,8 +153,6 @@ export async function run(entry, options = {}) {
|
|
|
130
153
|
|
|
131
154
|
// line break before fragment logs
|
|
132
155
|
log.message();
|
|
133
|
-
|
|
134
|
-
return server;
|
|
135
156
|
} catch (error) {
|
|
136
157
|
// line break before error
|
|
137
158
|
log.message();
|
|
@@ -7,6 +7,7 @@ class CanvasRecorder {
|
|
|
7
7
|
duration = Infinity,
|
|
8
8
|
framerate = 25,
|
|
9
9
|
quality = 100,
|
|
10
|
+
format,
|
|
10
11
|
onStart = noop,
|
|
11
12
|
onTick = noop,
|
|
12
13
|
onComplete = noop,
|
|
@@ -16,6 +17,7 @@ class CanvasRecorder {
|
|
|
16
17
|
this.framerate = framerate;
|
|
17
18
|
this.duration = duration;
|
|
18
19
|
this.quality = quality;
|
|
20
|
+
this.format = format;
|
|
19
21
|
this.onStart = onStart;
|
|
20
22
|
this.onTick = onTick;
|
|
21
23
|
this.onComplete = onComplete;
|
|
@@ -24,7 +26,7 @@ class CanvasRecorder {
|
|
|
24
26
|
this.deltaTime = 1000 / this.framerate;
|
|
25
27
|
|
|
26
28
|
this.frameDuration = 1000 / this.framerate;
|
|
27
|
-
this.frameTotal = isFinite(duration)
|
|
29
|
+
this.frameTotal = isFinite(this.duration)
|
|
28
30
|
? this.duration * this.framerate
|
|
29
31
|
: Infinity;
|
|
30
32
|
this.started = false;
|
|
@@ -9,7 +9,17 @@ class GIFRecorder extends CanvasRecorder {
|
|
|
9
9
|
this.tmpCanvas = document.createElement('canvas');
|
|
10
10
|
this.tmpContext = this.tmpCanvas.getContext('2d');
|
|
11
11
|
|
|
12
|
-
this.maxColors = Math.floor(map(this.quality,
|
|
12
|
+
this.maxColors = Math.floor(map(this.quality, 20, 100, 32, 256));
|
|
13
|
+
|
|
14
|
+
if (this.framerate > 50) {
|
|
15
|
+
console.warn(`GIFRecorder :: recording was capped at 50fps.`);
|
|
16
|
+
this.framerate = 50;
|
|
17
|
+
this.deltaTime = 1000 / this.framerate;
|
|
18
|
+
this.frameDuration = 1000 / this.framerate;
|
|
19
|
+
this.frameTotal = isFinite(this.duration)
|
|
20
|
+
? this.duration * this.framerate
|
|
21
|
+
: Infinity;
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
super.start();
|
|
15
25
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import CanvasRecorder from './CanvasRecorder.js';
|
|
2
|
+
import {
|
|
3
|
+
Output,
|
|
4
|
+
Mp4OutputFormat,
|
|
5
|
+
MkvOutputFormat,
|
|
6
|
+
MovOutputFormat,
|
|
7
|
+
WebMInputFormat,
|
|
8
|
+
BufferTarget,
|
|
9
|
+
CanvasSource,
|
|
10
|
+
Quality,
|
|
11
|
+
QUALITY_VERY_LOW,
|
|
12
|
+
QUALITY_LOW,
|
|
13
|
+
QUALITY_MEDIUM,
|
|
14
|
+
QUALITY_HIGH,
|
|
15
|
+
QUALITY_VERY_HIGH,
|
|
16
|
+
WebMOutputFormat,
|
|
17
|
+
} from 'mediabunny';
|
|
18
|
+
import { map } from '@fragment/utils/math.utils.js';
|
|
19
|
+
import { VIDEO_FORMATS } from '@fragment/state/exports.svelte.js';
|
|
20
|
+
|
|
21
|
+
class MediaBunnyRecorder extends CanvasRecorder {
|
|
22
|
+
/** @type Quality[] */
|
|
23
|
+
static BITRATES = [
|
|
24
|
+
QUALITY_VERY_LOW,
|
|
25
|
+
QUALITY_LOW,
|
|
26
|
+
QUALITY_MEDIUM,
|
|
27
|
+
QUALITY_HIGH,
|
|
28
|
+
QUALITY_VERY_HIGH,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* @param {HTMLCanvasElement} canvas
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {codec} options.string
|
|
36
|
+
*/
|
|
37
|
+
constructor(canvas, { codec, ...options }) {
|
|
38
|
+
super(canvas, options);
|
|
39
|
+
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
this.codec = codec;
|
|
42
|
+
|
|
43
|
+
const outputFormats = new Map();
|
|
44
|
+
outputFormats.set(VIDEO_FORMATS.MKV, MkvOutputFormat);
|
|
45
|
+
outputFormats.set(VIDEO_FORMATS.MP4, Mp4OutputFormat);
|
|
46
|
+
outputFormats.set(VIDEO_FORMATS.MOV, MovOutputFormat);
|
|
47
|
+
outputFormats.set(VIDEO_FORMATS.WEBM, WebMOutputFormat);
|
|
48
|
+
|
|
49
|
+
const outputFormat = outputFormats.get(this.format);
|
|
50
|
+
|
|
51
|
+
/** @type {Output} */
|
|
52
|
+
this.output = new Output({
|
|
53
|
+
format: new outputFormat(),
|
|
54
|
+
target: new BufferTarget(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { BITRATES } = MediaBunnyRecorder;
|
|
58
|
+
|
|
59
|
+
const bitrate =
|
|
60
|
+
BITRATES[
|
|
61
|
+
Math.floor(map(this.quality, 1, 100, 0, BITRATES.length - 1))
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** @type {CanvasSource} */
|
|
65
|
+
this.videoSource = new CanvasSource(this.canvas, {
|
|
66
|
+
codec,
|
|
67
|
+
bitrate,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.output.addVideoTrack(this.videoSource, {
|
|
71
|
+
frameRate: this.framerate,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async load() {
|
|
76
|
+
await this.output.start();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async tick({ frameCount, time }) {
|
|
80
|
+
const timestamp = frameCount / this.framerate;
|
|
81
|
+
|
|
82
|
+
this.videoSource.add(timestamp, this.frameDuration / 1000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async end() {
|
|
86
|
+
await this.output.finalize();
|
|
87
|
+
|
|
88
|
+
const { mimeType } = this.output.format;
|
|
89
|
+
const { buffer } = this.output.target;
|
|
90
|
+
|
|
91
|
+
this.result = new Blob([buffer], { type: mimeType });
|
|
92
|
+
|
|
93
|
+
super.end();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default MediaBunnyRecorder;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
IMAGE_ENCODINGS,
|
|
8
8
|
VIDEO_FORMATS,
|
|
9
9
|
exports,
|
|
10
|
+
getCodecsForFormat,
|
|
10
11
|
} from '../state/exports.svelte';
|
|
11
12
|
|
|
12
13
|
let { id = layout.getID(), headless = false } = $props();
|
|
@@ -81,16 +82,39 @@
|
|
|
81
82
|
params={{ options: Object.values(VIDEO_FORMATS) }}
|
|
82
83
|
onchange={(value) => {
|
|
83
84
|
exports.videoFormat = value;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
!getCodecsForFormat(exports.videoFormat).includes(
|
|
88
|
+
exports.videoCodec,
|
|
89
|
+
)
|
|
90
|
+
) {
|
|
91
|
+
exports.videoCodec = getCodecsForFormat(
|
|
92
|
+
exports.videoFormat,
|
|
93
|
+
)?.[0];
|
|
94
|
+
}
|
|
84
95
|
}}
|
|
85
96
|
/>
|
|
97
|
+
{#if [VIDEO_FORMATS.MKV, VIDEO_FORMATS.MP4, VIDEO_FORMATS.WEBM, VIDEO_FORMATS.MOV].includes(exports.videoFormat)}
|
|
98
|
+
<Field
|
|
99
|
+
key="codec"
|
|
100
|
+
value={exports.videoCodec}
|
|
101
|
+
params={{ options: getCodecsForFormat(exports.videoFormat) }}
|
|
102
|
+
onchange={(value) => {
|
|
103
|
+
exports.videoCodec = value;
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
{/if}
|
|
86
107
|
<Field
|
|
87
108
|
key="quality"
|
|
88
109
|
value={exports.videoQuality}
|
|
89
110
|
params={{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
options: [
|
|
112
|
+
{ value: 100, label: 'very high' },
|
|
113
|
+
{ value: 80, label: 'high' },
|
|
114
|
+
{ value: 60, label: 'medium' },
|
|
115
|
+
{ value: 40, label: 'low' },
|
|
116
|
+
{ value: 20, label: 'very low' },
|
|
117
|
+
],
|
|
94
118
|
triggerable: false,
|
|
95
119
|
}}
|
|
96
120
|
onchange={(value) => {
|
|
@@ -6,13 +6,47 @@ export const IMAGE_ENCODINGS = ['png', 'jpeg', 'webp'];
|
|
|
6
6
|
export const VIDEO_FORMATS = {
|
|
7
7
|
FRAMES: 'frames',
|
|
8
8
|
MP4: 'mp4',
|
|
9
|
-
GIF: 'gif',
|
|
10
9
|
WEBM: 'webm',
|
|
10
|
+
GIF: 'gif',
|
|
11
|
+
MKV: 'mkv',
|
|
12
|
+
MOV: 'mov',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const VIDEO_CODECS = {
|
|
16
|
+
AVC: 'avc',
|
|
17
|
+
HEVC: 'hevc',
|
|
18
|
+
VP8: 'vp8',
|
|
19
|
+
VP9: 'vp9',
|
|
20
|
+
AV1: 'av1',
|
|
11
21
|
};
|
|
12
22
|
|
|
23
|
+
export const VIDEO_CODECS_FORMATS = new Map();
|
|
24
|
+
VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.AVC, ['mp4', 'mkv', 'mov']);
|
|
25
|
+
VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.HEVC, ['mp4', 'mkv', 'mov']);
|
|
26
|
+
VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.VP8, ['webm', 'mkv']);
|
|
27
|
+
VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.VP9, ['webm', 'mkv']);
|
|
28
|
+
VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.AV1, ['webm', 'mkv']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* List valid codecs based on format
|
|
32
|
+
* @param {string} format
|
|
33
|
+
* @return {string[]}
|
|
34
|
+
*/
|
|
35
|
+
export function getCodecsForFormat(format) {
|
|
36
|
+
const codecs = [];
|
|
37
|
+
|
|
38
|
+
for (const [codec, formats] of VIDEO_CODECS_FORMATS) {
|
|
39
|
+
if (formats.includes(format)) {
|
|
40
|
+
codecs.push(codec);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return codecs;
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
class Exports {
|
|
14
48
|
imageEncoding = $state(IMAGE_ENCODINGS[0]);
|
|
15
|
-
videoFormat = $state(
|
|
49
|
+
videoFormat = $state(VIDEO_FORMATS.MP4);
|
|
16
50
|
pixelsPerInch = $state(72);
|
|
17
51
|
framerate = $state(60);
|
|
18
52
|
useDuration = $state(true);
|
|
@@ -20,6 +54,7 @@ class Exports {
|
|
|
20
54
|
loopCount = $state(1);
|
|
21
55
|
imageQuality = $state(100);
|
|
22
56
|
videoQuality = $state(100);
|
|
57
|
+
videoCodec = $state(VIDEO_CODECS.HEVC);
|
|
23
58
|
imageCount = $state(1);
|
|
24
59
|
recording = $state(false);
|
|
25
60
|
capturing = $state(false);
|
|
@@ -41,6 +76,7 @@ class Exports {
|
|
|
41
76
|
loopCount: this.loopCount,
|
|
42
77
|
imageQuality: this.imageQuality,
|
|
43
78
|
videoQuality: this.videoQuality,
|
|
79
|
+
videoCodec: this.videoCodec,
|
|
44
80
|
imageCount: this.imageCount,
|
|
45
81
|
videoCollapsed: this.videoCollapsed,
|
|
46
82
|
imageCollapsed: this.imageCollapsed,
|
|
@@ -105,6 +141,7 @@ class Exports {
|
|
|
105
141
|
format = this.videoFormat,
|
|
106
142
|
imageEncoding = this.imageEncoding,
|
|
107
143
|
quality = this.videoQuality,
|
|
144
|
+
codec = this.videoCodec,
|
|
108
145
|
duration,
|
|
109
146
|
filename,
|
|
110
147
|
pattern,
|
|
@@ -133,6 +170,7 @@ class Exports {
|
|
|
133
170
|
onTick,
|
|
134
171
|
framerate,
|
|
135
172
|
format,
|
|
173
|
+
codec,
|
|
136
174
|
imageEncoding,
|
|
137
175
|
quality,
|
|
138
176
|
duration: duration * this.loopCount,
|
|
@@ -67,10 +67,14 @@
|
|
|
67
67
|
value(event);
|
|
68
68
|
onclick(event);
|
|
69
69
|
},
|
|
70
|
-
download: (event) => {
|
|
71
|
-
|
|
70
|
+
download: async (event) => {
|
|
71
|
+
try {
|
|
72
|
+
let [data, filename] = await value(event);
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
download(data, filename);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`Error while trying to download:`, error);
|
|
77
|
+
}
|
|
74
78
|
},
|
|
75
79
|
number: (event = {}) => {
|
|
76
80
|
const isValueInRange = event.value >= 0 && event.value <= 1;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
let gui = $derived(buildConfig.gui ?? {});
|
|
11
11
|
let layout = $derived(buildConfig.layout ?? {});
|
|
12
|
-
let headless = $derived(layout.headless ??
|
|
12
|
+
let headless = $derived(layout.headless ?? true);
|
|
13
13
|
let resizable = $derived(layout.resizable ?? false);
|
|
14
14
|
|
|
15
15
|
let guiOutput = $derived(gui.output ?? true);
|
|
@@ -110,7 +110,11 @@
|
|
|
110
110
|
|
|
111
111
|
let backgroundColor = $derived.by(() => {
|
|
112
112
|
if (layout.previewing) {
|
|
113
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
sketch?.buildConfig?.backgroundColor ??
|
|
115
|
+
sketch?.backgroundColor ??
|
|
116
|
+
'inherit'
|
|
117
|
+
);
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
return sketch?.backgroundColor ?? 'inherit';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
};
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
|
-
<div class="checkbox">
|
|
15
|
+
<div class="checkbox" class:disabled>
|
|
16
16
|
<input
|
|
17
17
|
class="input"
|
|
18
18
|
bind:checked={value}
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
onchange={handleChange}
|
|
21
21
|
disabled={disabled ? 'disabled' : null}
|
|
22
22
|
/>
|
|
23
|
-
<div class="checked" />
|
|
24
23
|
</div>
|
|
25
24
|
|
|
26
25
|
<style>
|
|
@@ -36,11 +35,12 @@
|
|
|
36
35
|
background-color: var(--fragment-input-background-color);
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
.checkbox:hover {
|
|
38
|
+
:global(body:not(.fragment-dragging)) .checkbox:not(.disabled):hover {
|
|
40
39
|
box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
.
|
|
42
|
+
:global(body:not(.fragment-dragging))
|
|
43
|
+
.checkbox:not(.disabled):focus-within {
|
|
44
44
|
box-shadow: 0 0 0 2px var(--fragment-accent-color);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
outline: 0;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
.
|
|
56
|
+
.checkbox:after {
|
|
57
|
+
content: '';
|
|
58
|
+
|
|
57
59
|
position: absolute;
|
|
58
60
|
left: 3px;
|
|
59
61
|
top: 3px;
|
|
@@ -67,11 +69,11 @@
|
|
|
67
69
|
pointer-events: none;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
.input:checked
|
|
72
|
+
.checkbox:has(.input:checked):after {
|
|
71
73
|
opacity: 1;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
.input:checked:disabled
|
|
76
|
+
.checkbox:has(.input:checked:disabled):after {
|
|
75
77
|
background-color: var(--fragment-color-disabled);
|
|
76
78
|
}
|
|
77
79
|
</style>
|