fragment-tools 0.2.9 → 0.2.10
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 +4 -4
- package/src/cli/createConfig.js +1 -1
- package/src/cli/plugins/save.js +7 -2
- 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/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/.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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fragment-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "A web development environment for creative coding",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,6 @@
|
|
|
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",
|
|
@@ -34,14 +33,15 @@
|
|
|
34
33
|
"glslify": "^7.1.1",
|
|
35
34
|
"is-unicode-supported": "^2.0.0",
|
|
36
35
|
"kleur": "^4.1.4",
|
|
36
|
+
"mediabunny": "^1.13.3",
|
|
37
|
+
"milliparsec": "^5.1.0",
|
|
37
38
|
"sade": "^1.7.4",
|
|
38
39
|
"svelte": "^5.19.0",
|
|
39
40
|
"vite": "^6.3.5",
|
|
40
|
-
"webm-writer": "^1.0.0",
|
|
41
41
|
"ws": "^8.2.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@changesets/cli": "^2.29.
|
|
44
|
+
"@changesets/cli": "^2.29.7",
|
|
45
45
|
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
|
|
46
46
|
"@types/p5": "^1.7.6",
|
|
47
47
|
"@types/three": "^0.174.0",
|
package/src/cli/createConfig.js
CHANGED
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;
|
|
@@ -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>
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import FieldInputRow from './FieldInputRow.svelte';
|
|
5
5
|
import TextInput from './TextInput.svelte';
|
|
6
6
|
|
|
7
|
-
let {
|
|
7
|
+
let {
|
|
8
|
+
value,
|
|
9
|
+
context = null,
|
|
10
|
+
key = '',
|
|
11
|
+
onchange,
|
|
12
|
+
disabled = false,
|
|
13
|
+
} = $props();
|
|
8
14
|
|
|
9
15
|
/** @type {HTMLImageElement} */
|
|
10
16
|
let img;
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
let reader = new FileReader();
|
|
27
|
-
let dragover = false;
|
|
33
|
+
let dragover = $state(false);
|
|
28
34
|
|
|
29
35
|
function handleUpload(event) {
|
|
30
36
|
event.preventDefault();
|
|
@@ -44,6 +50,7 @@
|
|
|
44
50
|
reader.onload = null;
|
|
45
51
|
|
|
46
52
|
value = e.target.result;
|
|
53
|
+
onchange(value);
|
|
47
54
|
};
|
|
48
55
|
reader.readAsDataURL(file);
|
|
49
56
|
|
|
@@ -67,33 +74,31 @@
|
|
|
67
74
|
|
|
68
75
|
<div
|
|
69
76
|
class="img-container"
|
|
77
|
+
role="group"
|
|
78
|
+
aria-label="Drop an image here to upload"
|
|
70
79
|
class:dragover
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
ondragover={handleDragover}
|
|
81
|
+
ondragleave={handleDragleave}
|
|
82
|
+
ondrop={handleUpload}
|
|
74
83
|
>
|
|
75
84
|
<FieldInputRow --grid-template-columns="1fr 0.5fr">
|
|
76
85
|
<div class="row">
|
|
77
|
-
<div class="preview"
|
|
86
|
+
<div class="preview">
|
|
78
87
|
<img class="img" src="" alt="" bind:this={img} />
|
|
88
|
+
<button class="preview-button" onclick={handleClick}>
|
|
89
|
+
<span class="visually-hidden">Upload</span>
|
|
90
|
+
</button>
|
|
79
91
|
<input
|
|
80
92
|
class="input"
|
|
81
93
|
type="file"
|
|
82
94
|
bind:this={input}
|
|
83
|
-
|
|
95
|
+
onchange={handleUpload}
|
|
84
96
|
disabled={disabled ? 'disabled' : null}
|
|
85
97
|
/>
|
|
86
98
|
</div>
|
|
87
99
|
<TextInput disabled value={displayUrl} />
|
|
88
100
|
</div>
|
|
89
|
-
<ButtonInput
|
|
90
|
-
label="change"
|
|
91
|
-
on:click={handleClick}
|
|
92
|
-
on:dragover={handleDragover}
|
|
93
|
-
on:dragleave={handleDragleave}
|
|
94
|
-
on:drop={handleUpload}
|
|
95
|
-
{disabled}
|
|
96
|
-
/>
|
|
101
|
+
<ButtonInput label="change" onclick={handleClick} {disabled} />
|
|
97
102
|
</FieldInputRow>
|
|
98
103
|
</div>
|
|
99
104
|
|
|
@@ -103,6 +108,8 @@
|
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
.preview {
|
|
111
|
+
position: relative;
|
|
112
|
+
|
|
106
113
|
width: calc(var(--fragment-input-height) * 1);
|
|
107
114
|
height: calc(var(--fragment-input-height) * 1);
|
|
108
115
|
display: grid;
|
|
@@ -116,6 +123,17 @@
|
|
|
116
123
|
overflow: hidden;
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
.preview-button {
|
|
127
|
+
position: absolute;
|
|
128
|
+
top: 0;
|
|
129
|
+
left: 0;
|
|
130
|
+
|
|
131
|
+
width: 100%;
|
|
132
|
+
height: 100%;
|
|
133
|
+
|
|
134
|
+
opacity: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
.row {
|
|
120
138
|
display: grid;
|
|
121
139
|
grid-template-columns: 20px auto;
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.select {
|
|
119
|
-
padding: 0
|
|
119
|
+
padding: 0 18px 0 var(--padding, 6px);
|
|
120
120
|
|
|
121
121
|
width: 100%;
|
|
122
122
|
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
outline: 0;
|
|
127
127
|
background-color: transparent;
|
|
128
128
|
opacity: 1;
|
|
129
|
+
text-overflow: ellipsis;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
.select-input:not(.disabled) .select {
|
|
@@ -3,10 +3,9 @@ https://github.com/mattdesl/canvas-sketch/blob/24f6bb2bbdfdfd72a698a0b8a0962ad84
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { VIDEO_FORMATS } from '../state/exports.svelte';
|
|
6
|
-
import WebMRecorder from '../lib/canvas-recorder/WebMRecorder';
|
|
7
|
-
import MP4Recorder from '../lib/canvas-recorder/MP4Recorder';
|
|
8
6
|
import GIFRecorder from '../lib/canvas-recorder/GIFRecorder';
|
|
9
7
|
import FrameRecorder from '../lib/canvas-recorder/FrameRecorder';
|
|
8
|
+
import MediaBunnyRecorder from '../lib/canvas-recorder/MediaBunnyRecorder';
|
|
10
9
|
import { exportCanvas } from '../lib/canvas-recorder/utils';
|
|
11
10
|
import { map } from './math.utils';
|
|
12
11
|
import { createDataURLFromBlob, saveFiles } from './file.utils';
|
|
@@ -101,28 +100,21 @@ export async function screenshotCanvas(
|
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
function
|
|
105
|
-
let recorder = new
|
|
103
|
+
function record(canvas, options) {
|
|
104
|
+
let recorder = new MediaBunnyRecorder(canvas, options);
|
|
106
105
|
recorder.start();
|
|
107
106
|
|
|
108
107
|
return recorder;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
function
|
|
112
|
-
let recorder = new MP4Recorder(canvas, options);
|
|
113
|
-
recorder.start();
|
|
114
|
-
|
|
115
|
-
return recorder;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function recordCanvasGIF(canvas, options) {
|
|
110
|
+
function recordGIF(canvas, options) {
|
|
119
111
|
let recorder = new GIFRecorder(canvas, options);
|
|
120
112
|
recorder.start();
|
|
121
113
|
|
|
122
114
|
return recorder;
|
|
123
115
|
}
|
|
124
116
|
|
|
125
|
-
function
|
|
117
|
+
function recordFrames(canvas, options) {
|
|
126
118
|
let recorder = new FrameRecorder(canvas, options);
|
|
127
119
|
recorder.start();
|
|
128
120
|
|
|
@@ -138,6 +130,7 @@ export function recordCanvas(
|
|
|
138
130
|
duration = Infinity,
|
|
139
131
|
quality = 100,
|
|
140
132
|
pattern = defaultFilenamePattern,
|
|
133
|
+
codec,
|
|
141
134
|
exportDir,
|
|
142
135
|
imageEncoding,
|
|
143
136
|
onStart = () => {},
|
|
@@ -189,6 +182,7 @@ export function recordCanvas(
|
|
|
189
182
|
|
|
190
183
|
const options = {
|
|
191
184
|
framerate,
|
|
185
|
+
format,
|
|
192
186
|
duration,
|
|
193
187
|
quality,
|
|
194
188
|
onStart,
|
|
@@ -198,14 +192,22 @@ export function recordCanvas(
|
|
|
198
192
|
|
|
199
193
|
let recorder;
|
|
200
194
|
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
if (
|
|
196
|
+
[
|
|
197
|
+
VIDEO_FORMATS.MKV,
|
|
198
|
+
VIDEO_FORMATS.MOV,
|
|
199
|
+
VIDEO_FORMATS.MP4,
|
|
200
|
+
VIDEO_FORMATS.WEBM,
|
|
201
|
+
].includes(format)
|
|
202
|
+
) {
|
|
203
|
+
recorder = record(canvas, {
|
|
204
|
+
...options,
|
|
205
|
+
codec,
|
|
206
|
+
});
|
|
205
207
|
} else if (format === VIDEO_FORMATS.GIF) {
|
|
206
|
-
recorder =
|
|
208
|
+
recorder = recordGIF(canvas, options);
|
|
207
209
|
} else if (format === VIDEO_FORMATS.FRAMES) {
|
|
208
|
-
recorder =
|
|
210
|
+
recorder = recordFrames(canvas, {
|
|
209
211
|
...options,
|
|
210
212
|
imageEncoding,
|
|
211
213
|
});
|
package/.changeset/README.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Changesets
|
|
2
|
-
|
|
3
|
-
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
-
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
-
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
|
-
|
|
7
|
-
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
-
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
package/.changeset/config.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
3
|
-
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "raphaelameaume/fragment"}],
|
|
4
|
-
"commit": false,
|
|
5
|
-
"fixed": [],
|
|
6
|
-
"linked": [],
|
|
7
|
-
"access": "restricted",
|
|
8
|
-
"baseBranch": "main",
|
|
9
|
-
"updateInternalDependencies": "patch",
|
|
10
|
-
"ignore": []
|
|
11
|
-
}
|