@alpha2-sh/editor 0.0.0 → 0.1.0
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/README.md +107 -1
- package/dist/browser/bridge.d.ts +44 -0
- package/dist/browser/bridge.js +2176 -0
- package/dist/browser/extensions-worker-entry.js +3 -0
- package/dist/browser/extensions-worker.d.ts +46 -0
- package/dist/browser/extensions-worker.js +149 -0
- package/dist/browser/hash-worker-entry.d.ts +1 -0
- package/dist/browser/hash-worker-entry.js +3 -0
- package/dist/browser/hash-worker.d.ts +24 -0
- package/dist/browser/hash-worker.js +87 -0
- package/dist/browser/index.d.ts +11 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/jobs.d.ts +82 -0
- package/dist/browser/jobs.js +291 -0
- package/dist/browser/resources.d.ts +40 -0
- package/dist/browser/resources.js +83 -0
- package/dist/browser/runtime/assets.d.ts +233 -0
- package/dist/browser/runtime/assets.js +633 -0
- package/dist/browser/runtime/audio.d.ts +60 -0
- package/dist/browser/runtime/audio.js +549 -0
- package/dist/browser/runtime/budget.d.ts +11 -0
- package/dist/browser/runtime/budget.js +26 -0
- package/dist/browser/runtime/caption-fonts.d.ts +34 -0
- package/dist/browser/runtime/caption-fonts.js +69 -0
- package/dist/browser/runtime/color.d.ts +7 -0
- package/dist/browser/runtime/color.js +12 -0
- package/dist/browser/runtime/engine.d.ts +91 -0
- package/dist/browser/runtime/engine.js +352 -0
- package/dist/browser/runtime/export-audio.d.ts +23 -0
- package/dist/browser/runtime/export-audio.js +75 -0
- package/dist/browser/runtime/exporter.d.ts +140 -0
- package/dist/browser/runtime/exporter.js +751 -0
- package/dist/browser/runtime/extension-import.d.ts +196 -0
- package/dist/browser/runtime/extension-import.js +521 -0
- package/dist/browser/runtime/fingerprint.d.ts +57 -0
- package/dist/browser/runtime/fingerprint.js +157 -0
- package/dist/browser/runtime/generators.d.ts +188 -0
- package/dist/browser/runtime/generators.js +173 -0
- package/dist/browser/runtime/locks.d.ts +32 -0
- package/dist/browser/runtime/locks.js +86 -0
- package/dist/browser/runtime/output.d.ts +87 -0
- package/dist/browser/runtime/output.js +230 -0
- package/dist/browser/runtime/preview-utils.d.ts +3 -0
- package/dist/browser/runtime/preview-utils.js +49 -0
- package/dist/browser/runtime/proxies.d.ts +72 -0
- package/dist/browser/runtime/proxies.js +369 -0
- package/dist/browser/runtime/proxy-storage.d.ts +59 -0
- package/dist/browser/runtime/proxy-storage.js +119 -0
- package/dist/browser/runtime/sources.d.ts +82 -0
- package/dist/browser/runtime/sources.js +819 -0
- package/dist/browser/runtime/staging.d.ts +10 -0
- package/dist/browser/runtime/staging.js +32 -0
- package/dist/browser/runtime/still.d.ts +21 -0
- package/dist/browser/runtime/still.js +52 -0
- package/dist/browser/runtime/wasm.d.ts +9 -0
- package/dist/browser/runtime/wasm.js +20 -0
- package/dist/browser/types.d.ts +549 -0
- package/dist/browser/types.js +1 -0
- package/dist/browser/worker-entry.d.ts +1 -0
- package/dist/browser/worker-entry.js +2 -0
- package/dist/browser/worker.d.ts +24 -0
- package/dist/browser/worker.js +239 -0
- package/dist/core/captions.d.ts +34 -0
- package/dist/core/captions.js +108 -0
- package/dist/core/clip-time.d.ts +31 -0
- package/dist/core/clip-time.js +125 -0
- package/dist/core/commands.d.ts +413 -0
- package/dist/core/commands.js +1118 -0
- package/dist/core/compositions.d.ts +234 -0
- package/dist/core/compositions.js +851 -0
- package/dist/core/document.d.ts +245 -0
- package/dist/core/document.js +683 -0
- package/dist/core/keyframes.d.ts +82 -0
- package/dist/core/keyframes.js +153 -0
- package/dist/core/presets.d.ts +64 -0
- package/dist/core/presets.js +241 -0
- package/dist/core/session.d.ts +150 -0
- package/dist/core/session.js +541 -0
- package/dist/core/text.d.ts +17 -0
- package/dist/core/text.js +17 -0
- package/dist/core/time.d.ts +19 -0
- package/dist/core/time.js +39 -0
- package/dist/core/timeline-index.d.ts +44 -0
- package/dist/core/timeline-index.js +59 -0
- package/dist/core/transitions.d.ts +20 -0
- package/dist/core/transitions.js +118 -0
- package/dist/core/validated.d.ts +23 -0
- package/dist/core/validated.js +52 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/native-client/index.d.ts +131 -0
- package/dist/native-client/index.js +354 -0
- package/dist/ports/index.d.ts +146 -0
- package/dist/ports/index.js +1 -0
- package/dist/react/index.d.ts +37 -0
- package/dist/react/index.js +94 -0
- package/package.json +69 -8
- /package/{index.js → dist/browser/extensions-worker-entry.d.ts} +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Evidence of an imported file's media, recorded in the project so any host can tell later whether
|
|
2
|
+
// media it finds is the same: the bytes' hash, their length and date, and what the demuxer sees.
|
|
3
|
+
// Whether two fingerprints match is the engine's decision (`alpha2/assets`), never made here.
|
|
4
|
+
import { sha256Blob } from "alpha2/assets";
|
|
5
|
+
import { colorMetadataFromVideoColorSpace, sampleAspectForStream, } from "alpha2/hdr-frames";
|
|
6
|
+
import { TICKS_PER_SECOND, validFrameRate } from "alpha2/project";
|
|
7
|
+
import { ALL_FORMATS, BlobSource, EncodedPacketSink, Input } from "mediabunny";
|
|
8
|
+
/**
|
|
9
|
+
* The fingerprint of `file` as media of `kind`. `size` is an image's decoded size; video and audio
|
|
10
|
+
* are probed. `hash` makes the content hash, `sha256Blob` on this thread unless the caller hashes
|
|
11
|
+
* elsewhere (`hashInWorker`).
|
|
12
|
+
* @param {File} file
|
|
13
|
+
* @param {string} kind
|
|
14
|
+
* @param {{ signal?: AbortSignal, onProgress?: (bytes: number) => void, size?: { width: number, height: number }, hash?: (file: Blob, options: { signal?: AbortSignal, onProgress?: (bytes: number) => void }) => Promise<string> }} [options]
|
|
15
|
+
* @returns {Promise<import("alpha2/project").MediaFingerprint>}
|
|
16
|
+
*/
|
|
17
|
+
export async function fingerprintFile(file, kind, options = {}) {
|
|
18
|
+
const hash = options.hash ?? sha256Blob;
|
|
19
|
+
const fingerprint = {
|
|
20
|
+
content_sha256: await hash(file, {
|
|
21
|
+
signal: options.signal,
|
|
22
|
+
onProgress: options.onProgress,
|
|
23
|
+
}),
|
|
24
|
+
byte_length: file.size,
|
|
25
|
+
modified_ms: file.lastModified,
|
|
26
|
+
};
|
|
27
|
+
if (kind === "image" && options.size)
|
|
28
|
+
return {
|
|
29
|
+
...fingerprint,
|
|
30
|
+
streams: {
|
|
31
|
+
video: { width: options.size.width, height: options.size.height },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
if (kind !== "video" && kind !== "audio")
|
|
35
|
+
return fingerprint;
|
|
36
|
+
return { ...fingerprint, ...(await probeEvidence(file)) };
|
|
37
|
+
}
|
|
38
|
+
/** What the demuxer sees: the primary streams' shapes, the duration, and a declared color. */
|
|
39
|
+
export async function probeEvidence(file) {
|
|
40
|
+
const input = new Input({
|
|
41
|
+
source: new BlobSource(file),
|
|
42
|
+
formats: ALL_FORMATS,
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
const video = await input.getPrimaryVideoTrack();
|
|
46
|
+
const audio = await input.getPrimaryAudioTrack();
|
|
47
|
+
const streams = {};
|
|
48
|
+
let color;
|
|
49
|
+
if (video) {
|
|
50
|
+
const rate = rationalRate(await presentedRate(video).catch(() => null));
|
|
51
|
+
const shown = await displaySize(video);
|
|
52
|
+
streams.video = {
|
|
53
|
+
width: shown.width,
|
|
54
|
+
height: shown.height,
|
|
55
|
+
...(rate ? { frame_rate: rate } : {}),
|
|
56
|
+
};
|
|
57
|
+
const space = await video.getColorSpace().catch(() => null);
|
|
58
|
+
if (space &&
|
|
59
|
+
(space.primaries || space.transfer || space.matrix || space.fullRange))
|
|
60
|
+
color = colorMetadataFromVideoColorSpace(space);
|
|
61
|
+
}
|
|
62
|
+
if (audio)
|
|
63
|
+
streams.audio = {
|
|
64
|
+
channels: audio.numberOfChannels,
|
|
65
|
+
sample_rate: audio.sampleRate,
|
|
66
|
+
};
|
|
67
|
+
// The latest end of any track: a stream starting at 1.4 s and lasting 2 s ends at 3.4 s.
|
|
68
|
+
const seconds = await input.computeDuration().catch(() => null);
|
|
69
|
+
return {
|
|
70
|
+
...(video || audio ? { streams } : {}),
|
|
71
|
+
...(Number.isFinite(seconds)
|
|
72
|
+
? { duration_ticks: Math.round(seconds * TICKS_PER_SECOND) }
|
|
73
|
+
: {}),
|
|
74
|
+
...(color ? { color } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
input.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The size a video track is shown at, in square pixels and turned as it is shown, as every host
|
|
83
|
+
* finds it: the sample aspect ratio is the container's when it declares one that is not square,
|
|
84
|
+
* else the one the H.264 or HEVC parameter sets signal (FFmpeg's `av_guess_sample_aspect_ratio`);
|
|
85
|
+
* mediabunny reads only the container's. The stretch is the engine's
|
|
86
|
+
* (`SampleAspectRatio::square_pixel_size`): the longer side of a sample grows, rounded to the
|
|
87
|
+
* nearest pixel.
|
|
88
|
+
* @param {import("mediabunny").InputVideoTrack} track
|
|
89
|
+
* @returns {Promise<{ width: number, height: number }>}
|
|
90
|
+
*/
|
|
91
|
+
export async function displaySize(track) {
|
|
92
|
+
const declared = await track.getPixelAspectRatio().catch(() => null);
|
|
93
|
+
let aspect = declared && declared.num !== declared.den ? declared : undefined;
|
|
94
|
+
if (!aspect) {
|
|
95
|
+
const config = await track.getDecoderConfig().catch(() => null);
|
|
96
|
+
const description = config?.description ?? undefined;
|
|
97
|
+
const first = description === undefined
|
|
98
|
+
? await new EncodedPacketSink(track).getFirstPacket().catch(() => null)
|
|
99
|
+
: null;
|
|
100
|
+
aspect = sampleAspectForStream(config?.codec ?? track.codec, description, first?.data);
|
|
101
|
+
}
|
|
102
|
+
let width = track.codedWidth;
|
|
103
|
+
let height = track.codedHeight;
|
|
104
|
+
if (aspect && aspect.num > aspect.den)
|
|
105
|
+
width = Math.floor((width * aspect.num * 2 + aspect.den) / (2 * aspect.den));
|
|
106
|
+
else if (aspect && aspect.num < aspect.den)
|
|
107
|
+
height = Math.floor((height * aspect.den * 2 + aspect.num) / (2 * aspect.num));
|
|
108
|
+
return track.rotation % 180 === 0
|
|
109
|
+
? { width, height }
|
|
110
|
+
: { width: height, height: width };
|
|
111
|
+
}
|
|
112
|
+
/** How many frames the rate is measured over, at least: about four seconds at 30 fps. */
|
|
113
|
+
const RATE_FRAMES = 120;
|
|
114
|
+
/**
|
|
115
|
+
* The rate `track` presents its first frames at, from packet times alone (durations differ between
|
|
116
|
+
* demuxers); past {@link RATE_FRAMES}, packets run up to the first presented after every one
|
|
117
|
+
* taken, so reordered frames stay in the span. `null` with fewer than two frames or no span. Twin
|
|
118
|
+
* of the engine's `alpha2_assets::FrameRateEvidence` (held equal by
|
|
119
|
+
* `tests/cases/media-evidence-corpus.json`); a declared rate is no common ground across hosts.
|
|
120
|
+
* @param {import("mediabunny").InputVideoTrack} track
|
|
121
|
+
* @returns {Promise<number | null>}
|
|
122
|
+
*/
|
|
123
|
+
async function presentedRate(track) {
|
|
124
|
+
let count = 0;
|
|
125
|
+
let first = Infinity;
|
|
126
|
+
let last = -Infinity;
|
|
127
|
+
for await (const packet of new EncodedPacketSink(track).packets(undefined, undefined, { metadataOnly: true })) {
|
|
128
|
+
const time = packet.timestamp;
|
|
129
|
+
if (!Number.isFinite(time))
|
|
130
|
+
continue;
|
|
131
|
+
if (count >= RATE_FRAMES && time > last)
|
|
132
|
+
break;
|
|
133
|
+
first = Math.min(first, time);
|
|
134
|
+
last = Math.max(last, time);
|
|
135
|
+
count += 1;
|
|
136
|
+
}
|
|
137
|
+
return count >= 2 && last > first ? (count - 1) / (last - first) : null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A measured frame rate as the rational a stream declares: the NTSC rates and whole rates exactly,
|
|
141
|
+
* others to a thousandth (the engine's `alpha2_assets::rational_rate` is its twin). `null` when the
|
|
142
|
+
* measure is no rate a project holds (so slow it rounds to no frames, or a frame shorter than a
|
|
143
|
+
* tick, which only broken timestamps give): the fingerprint then records no rate rather than one
|
|
144
|
+
* the engine refuses.
|
|
145
|
+
*/
|
|
146
|
+
export function rationalRate(rate) {
|
|
147
|
+
if (!Number.isFinite(rate) || rate <= 0)
|
|
148
|
+
return null;
|
|
149
|
+
for (const base of [24, 30, 48, 60, 120]) {
|
|
150
|
+
if (Math.abs(rate - (base * 1000) / 1001) < 0.005)
|
|
151
|
+
return { num: base * 1000, den: 1001 };
|
|
152
|
+
}
|
|
153
|
+
const rational = Math.abs(rate - Math.round(rate)) < 0.005
|
|
154
|
+
? { num: Math.round(rate), den: 1 }
|
|
155
|
+
: { num: Math.round(rate * 1000), den: 1000 };
|
|
156
|
+
return validFrameRate(rational) ? rational : null;
|
|
157
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export function defaultGenerator(kind?: string): {
|
|
2
|
+
kind: string;
|
|
3
|
+
colors: string[];
|
|
4
|
+
angle: number;
|
|
5
|
+
radius: number;
|
|
6
|
+
size: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
cornerRadius: number;
|
|
11
|
+
rotation: number;
|
|
12
|
+
fill: boolean;
|
|
13
|
+
stroke: {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
color: string;
|
|
16
|
+
width: number;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/** The `alpha2_model::Generator` for an editor description. */
|
|
20
|
+
export function encodeGenerator(generator: any): {
|
|
21
|
+
type: string;
|
|
22
|
+
paint: {
|
|
23
|
+
type: string;
|
|
24
|
+
color: {
|
|
25
|
+
red: number;
|
|
26
|
+
green: number;
|
|
27
|
+
blue: number;
|
|
28
|
+
alpha: number;
|
|
29
|
+
};
|
|
30
|
+
start?: undefined;
|
|
31
|
+
end?: undefined;
|
|
32
|
+
stops?: undefined;
|
|
33
|
+
center?: undefined;
|
|
34
|
+
radius?: undefined;
|
|
35
|
+
angle_degrees?: undefined;
|
|
36
|
+
};
|
|
37
|
+
shape?: undefined;
|
|
38
|
+
center?: undefined;
|
|
39
|
+
size?: undefined;
|
|
40
|
+
rotation_degrees?: undefined;
|
|
41
|
+
fill?: undefined;
|
|
42
|
+
stroke?: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
type: string;
|
|
45
|
+
paint: {
|
|
46
|
+
type: string;
|
|
47
|
+
start: {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
};
|
|
51
|
+
end: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
};
|
|
55
|
+
stops: {
|
|
56
|
+
offset: number;
|
|
57
|
+
color: {
|
|
58
|
+
red: number;
|
|
59
|
+
green: number;
|
|
60
|
+
blue: number;
|
|
61
|
+
alpha: number;
|
|
62
|
+
};
|
|
63
|
+
}[];
|
|
64
|
+
color?: undefined;
|
|
65
|
+
center?: undefined;
|
|
66
|
+
radius?: undefined;
|
|
67
|
+
angle_degrees?: undefined;
|
|
68
|
+
};
|
|
69
|
+
shape?: undefined;
|
|
70
|
+
center?: undefined;
|
|
71
|
+
size?: undefined;
|
|
72
|
+
rotation_degrees?: undefined;
|
|
73
|
+
fill?: undefined;
|
|
74
|
+
stroke?: undefined;
|
|
75
|
+
} | {
|
|
76
|
+
type: string;
|
|
77
|
+
paint: {
|
|
78
|
+
type: string;
|
|
79
|
+
center: {
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
};
|
|
83
|
+
radius: number;
|
|
84
|
+
stops: {
|
|
85
|
+
offset: number;
|
|
86
|
+
color: {
|
|
87
|
+
red: number;
|
|
88
|
+
green: number;
|
|
89
|
+
blue: number;
|
|
90
|
+
alpha: number;
|
|
91
|
+
};
|
|
92
|
+
}[];
|
|
93
|
+
color?: undefined;
|
|
94
|
+
start?: undefined;
|
|
95
|
+
end?: undefined;
|
|
96
|
+
angle_degrees?: undefined;
|
|
97
|
+
};
|
|
98
|
+
shape?: undefined;
|
|
99
|
+
center?: undefined;
|
|
100
|
+
size?: undefined;
|
|
101
|
+
rotation_degrees?: undefined;
|
|
102
|
+
fill?: undefined;
|
|
103
|
+
stroke?: undefined;
|
|
104
|
+
} | {
|
|
105
|
+
type: string;
|
|
106
|
+
paint: {
|
|
107
|
+
type: string;
|
|
108
|
+
center: {
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
};
|
|
112
|
+
angle_degrees: any;
|
|
113
|
+
stops: {
|
|
114
|
+
offset: number;
|
|
115
|
+
color: {
|
|
116
|
+
red: number;
|
|
117
|
+
green: number;
|
|
118
|
+
blue: number;
|
|
119
|
+
alpha: number;
|
|
120
|
+
};
|
|
121
|
+
}[];
|
|
122
|
+
color?: undefined;
|
|
123
|
+
start?: undefined;
|
|
124
|
+
end?: undefined;
|
|
125
|
+
radius?: undefined;
|
|
126
|
+
};
|
|
127
|
+
shape?: undefined;
|
|
128
|
+
center?: undefined;
|
|
129
|
+
size?: undefined;
|
|
130
|
+
rotation_degrees?: undefined;
|
|
131
|
+
fill?: undefined;
|
|
132
|
+
stroke?: undefined;
|
|
133
|
+
} | {
|
|
134
|
+
type: string;
|
|
135
|
+
shape: {
|
|
136
|
+
type: string;
|
|
137
|
+
corner_radius: number;
|
|
138
|
+
} | {
|
|
139
|
+
type: string;
|
|
140
|
+
corner_radius?: undefined;
|
|
141
|
+
};
|
|
142
|
+
center: {
|
|
143
|
+
x: number;
|
|
144
|
+
y: number;
|
|
145
|
+
};
|
|
146
|
+
size: {
|
|
147
|
+
x: number;
|
|
148
|
+
y: number;
|
|
149
|
+
};
|
|
150
|
+
rotation_degrees: any;
|
|
151
|
+
fill: {
|
|
152
|
+
type: string;
|
|
153
|
+
color: {
|
|
154
|
+
red: number;
|
|
155
|
+
green: number;
|
|
156
|
+
blue: number;
|
|
157
|
+
alpha: number;
|
|
158
|
+
};
|
|
159
|
+
} | null;
|
|
160
|
+
stroke: {
|
|
161
|
+
width: number;
|
|
162
|
+
paint: {
|
|
163
|
+
type: string;
|
|
164
|
+
color: {
|
|
165
|
+
red: number;
|
|
166
|
+
green: number;
|
|
167
|
+
blue: number;
|
|
168
|
+
alpha: number;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
} | null;
|
|
172
|
+
paint?: undefined;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Draws an approximation of the generator for a library thumbnail. Canvas gradients interpolate in
|
|
176
|
+
* sRGB where the engine uses linear light, so the middle of a gradient is a little darker here than
|
|
177
|
+
* in the preview.
|
|
178
|
+
*/
|
|
179
|
+
export function drawGenerator(context: any, generator: any, width: any, height: any): void;
|
|
180
|
+
/** Checks a generator read from a project file. */
|
|
181
|
+
export function isGenerator(value: any): boolean;
|
|
182
|
+
export const GENERATOR_KINDS: {
|
|
183
|
+
value: string;
|
|
184
|
+
label: string;
|
|
185
|
+
}[];
|
|
186
|
+
export function isShape(kind: any): boolean;
|
|
187
|
+
export function isGradient(kind: any): boolean;
|
|
188
|
+
export function generatorLabel(kind: any): string;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Generated sources: pictures the engine draws itself (`kind: "generated"`). Positions are
|
|
2
|
+
// normalized ((0, 0) top-left, (1, 1) bottom-right); radii and strokes are fractions of the shorter
|
|
3
|
+
// side.
|
|
4
|
+
import { rgba } from "./color.js";
|
|
5
|
+
export const GENERATOR_KINDS = [
|
|
6
|
+
{ value: "solid", label: "Solid color" },
|
|
7
|
+
{ value: "linear", label: "Linear gradient" },
|
|
8
|
+
{ value: "radial", label: "Radial gradient" },
|
|
9
|
+
{ value: "conic", label: "Conic gradient" },
|
|
10
|
+
{ value: "rectangle", label: "Rectangle" },
|
|
11
|
+
{ value: "ellipse", label: "Ellipse" },
|
|
12
|
+
];
|
|
13
|
+
export const isShape = (kind) => kind === "rectangle" || kind === "ellipse";
|
|
14
|
+
export const isGradient = (kind) => kind === "linear" || kind === "radial" || kind === "conic";
|
|
15
|
+
export const generatorLabel = (kind) => GENERATOR_KINDS.find((item) => item.value === kind)?.label ?? "Generated";
|
|
16
|
+
export function defaultGenerator(kind = "linear") {
|
|
17
|
+
return {
|
|
18
|
+
kind,
|
|
19
|
+
colors: ["#6d78ff", "#12b5a4"],
|
|
20
|
+
// Degrees clockwise from up, as CSS gradients measure them.
|
|
21
|
+
angle: 135,
|
|
22
|
+
radius: 0.9,
|
|
23
|
+
size: { x: 0.6, y: 0.35 },
|
|
24
|
+
cornerRadius: 0.04,
|
|
25
|
+
rotation: 0,
|
|
26
|
+
fill: true,
|
|
27
|
+
stroke: { enabled: false, color: "#ffffff", width: 0.012 },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The `alpha2_model::Generator` for an editor description. */
|
|
31
|
+
export function encodeGenerator(generator) {
|
|
32
|
+
const [first, second] = generator.colors;
|
|
33
|
+
const stops = [
|
|
34
|
+
{ offset: 0, color: rgba(first) },
|
|
35
|
+
{ offset: 1, color: rgba(second) },
|
|
36
|
+
];
|
|
37
|
+
const center = { x: 0.5, y: 0.5 };
|
|
38
|
+
switch (generator.kind) {
|
|
39
|
+
case "solid":
|
|
40
|
+
return { type: "fill", paint: { type: "solid", color: rgba(first) } };
|
|
41
|
+
case "linear": {
|
|
42
|
+
const radians = (generator.angle * Math.PI) / 180;
|
|
43
|
+
const dx = 0.5 * Math.sin(radians);
|
|
44
|
+
const dy = -0.5 * Math.cos(radians);
|
|
45
|
+
return {
|
|
46
|
+
type: "fill",
|
|
47
|
+
paint: {
|
|
48
|
+
type: "linear_gradient",
|
|
49
|
+
start: { x: 0.5 - dx, y: 0.5 - dy },
|
|
50
|
+
end: { x: 0.5 + dx, y: 0.5 + dy },
|
|
51
|
+
stops,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
case "radial":
|
|
56
|
+
return {
|
|
57
|
+
type: "fill",
|
|
58
|
+
paint: {
|
|
59
|
+
type: "radial_gradient",
|
|
60
|
+
center,
|
|
61
|
+
radius: Math.max(0.01, generator.radius),
|
|
62
|
+
stops,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
case "conic":
|
|
66
|
+
return {
|
|
67
|
+
type: "fill",
|
|
68
|
+
paint: {
|
|
69
|
+
type: "conic_gradient",
|
|
70
|
+
center,
|
|
71
|
+
angle_degrees: generator.angle,
|
|
72
|
+
stops,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
default: {
|
|
76
|
+
const stroked = generator.stroke.enabled;
|
|
77
|
+
const filled = generator.fill || !stroked;
|
|
78
|
+
return {
|
|
79
|
+
type: "shape",
|
|
80
|
+
shape: generator.kind === "rectangle"
|
|
81
|
+
? {
|
|
82
|
+
type: "rectangle",
|
|
83
|
+
corner_radius: Math.max(0, generator.cornerRadius),
|
|
84
|
+
}
|
|
85
|
+
: { type: "ellipse" },
|
|
86
|
+
center,
|
|
87
|
+
size: {
|
|
88
|
+
x: Math.max(0, generator.size.x),
|
|
89
|
+
y: Math.max(0, generator.size.y),
|
|
90
|
+
},
|
|
91
|
+
rotation_degrees: generator.rotation,
|
|
92
|
+
fill: filled ? { type: "solid", color: rgba(first) } : null,
|
|
93
|
+
stroke: stroked
|
|
94
|
+
? {
|
|
95
|
+
width: Math.max(0.001, generator.stroke.width),
|
|
96
|
+
paint: { type: "solid", color: rgba(generator.stroke.color) },
|
|
97
|
+
}
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Draws an approximation of the generator for a library thumbnail. Canvas gradients interpolate in
|
|
105
|
+
* sRGB where the engine uses linear light, so the middle of a gradient is a little darker here than
|
|
106
|
+
* in the preview.
|
|
107
|
+
*/
|
|
108
|
+
export function drawGenerator(context, generator, width, height) {
|
|
109
|
+
const [first, second] = generator.colors;
|
|
110
|
+
const short = Math.min(width, height);
|
|
111
|
+
const gradient = (paint) => {
|
|
112
|
+
paint.addColorStop(0, first);
|
|
113
|
+
paint.addColorStop(1, second);
|
|
114
|
+
return paint;
|
|
115
|
+
};
|
|
116
|
+
context.clearRect(0, 0, width, height);
|
|
117
|
+
switch (generator.kind) {
|
|
118
|
+
case "solid":
|
|
119
|
+
context.fillStyle = first;
|
|
120
|
+
context.fillRect(0, 0, width, height);
|
|
121
|
+
return;
|
|
122
|
+
case "linear": {
|
|
123
|
+
const radians = (generator.angle * Math.PI) / 180;
|
|
124
|
+
const dx = 0.5 * Math.sin(radians) * width;
|
|
125
|
+
const dy = -0.5 * Math.cos(radians) * height;
|
|
126
|
+
context.fillStyle = gradient(context.createLinearGradient(width / 2 - dx, height / 2 - dy, width / 2 + dx, height / 2 + dy));
|
|
127
|
+
context.fillRect(0, 0, width, height);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "radial":
|
|
131
|
+
context.fillStyle = gradient(context.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, generator.radius * short));
|
|
132
|
+
context.fillRect(0, 0, width, height);
|
|
133
|
+
return;
|
|
134
|
+
case "conic":
|
|
135
|
+
// Canvas measures from the +x axis; the engine, like CSS, from up.
|
|
136
|
+
context.fillStyle = gradient(context.createConicGradient(((generator.angle - 90) * Math.PI) / 180, width / 2, height / 2));
|
|
137
|
+
context.fillRect(0, 0, width, height);
|
|
138
|
+
return;
|
|
139
|
+
default: {
|
|
140
|
+
const w = generator.size.x * width;
|
|
141
|
+
const h = generator.size.y * height;
|
|
142
|
+
context.save();
|
|
143
|
+
context.translate(width / 2, height / 2);
|
|
144
|
+
context.rotate((generator.rotation * Math.PI) / 180);
|
|
145
|
+
context.beginPath();
|
|
146
|
+
if (generator.kind === "rectangle")
|
|
147
|
+
context.roundRect(-w / 2, -h / 2, w, h, Math.min(generator.cornerRadius * short, w / 2, h / 2));
|
|
148
|
+
else
|
|
149
|
+
context.ellipse(0, 0, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
150
|
+
if (generator.fill || !generator.stroke.enabled) {
|
|
151
|
+
context.fillStyle = first;
|
|
152
|
+
context.fill();
|
|
153
|
+
}
|
|
154
|
+
if (generator.stroke.enabled) {
|
|
155
|
+
context.strokeStyle = generator.stroke.color;
|
|
156
|
+
context.lineWidth = Math.max(1, generator.stroke.width * short);
|
|
157
|
+
context.stroke();
|
|
158
|
+
}
|
|
159
|
+
context.restore();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Checks a generator read from a project file. */
|
|
164
|
+
export function isGenerator(value) {
|
|
165
|
+
return (value !== null &&
|
|
166
|
+
typeof value === "object" &&
|
|
167
|
+
GENERATOR_KINDS.some((item) => item.value === value.kind) &&
|
|
168
|
+
Array.isArray(value.colors) &&
|
|
169
|
+
value.colors.length === 2 &&
|
|
170
|
+
[value.angle, value.radius, value.cornerRadius, value.rotation].every(Number.isFinite) &&
|
|
171
|
+
typeof value.size === "object" &&
|
|
172
|
+
typeof value.stroke === "object");
|
|
173
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds the shared lock `name` until `release()`. Resolves once held; where the browser has no Web
|
|
3
|
+
* Locks, or refuses the lock, at once and holding nothing. `release()` returns a promise that
|
|
4
|
+
* settles when the lock is let go.
|
|
5
|
+
* @param {string} name
|
|
6
|
+
* @returns {Promise<{ release(): Promise<void> }>}
|
|
7
|
+
*/
|
|
8
|
+
export function holdShared(name: string): Promise<{
|
|
9
|
+
release(): Promise<void>;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Runs `remove` holding the lock `name` exclusively, only if no page holds it, and resolves to
|
|
13
|
+
* whether it ran. Without Web Locks it runs.
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @param {() => Promise<void>} remove
|
|
16
|
+
* @returns {Promise<boolean>}
|
|
17
|
+
*/
|
|
18
|
+
export function whenUnheld(name: string, remove: () => Promise<void>): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Runs `body` holding the lock `name` exclusively, waiting for any page that holds it, and resolves
|
|
21
|
+
* to what `body` resolves to; `body` is told whether it holds the lock. Where the browser has no
|
|
22
|
+
* Web Locks, or refuses the lock, `body` runs at once, holding nothing. Rejects with
|
|
23
|
+
* `signal.reason` once `signal` aborts while it waits.
|
|
24
|
+
* @template T
|
|
25
|
+
* @param {string} name
|
|
26
|
+
* @param {{ signal?: AbortSignal }} options
|
|
27
|
+
* @param {(held: boolean) => Promise<T>} body
|
|
28
|
+
* @returns {Promise<T>}
|
|
29
|
+
*/
|
|
30
|
+
export function withExclusive<T>(name: string, { signal }: {
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
} | undefined, body: (held: boolean) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Web Locks marking OPFS entries in use: users hold the lock shared, removers take it exclusively
|
|
2
|
+
// only if unheld. Closed pages release their locks; without Web Locks nothing is held.
|
|
3
|
+
const locks = () => globalThis.navigator?.locks ?? null;
|
|
4
|
+
/**
|
|
5
|
+
* Holds the shared lock `name` until `release()`. Resolves once held; where the browser has no Web
|
|
6
|
+
* Locks, or refuses the lock, at once and holding nothing. `release()` returns a promise that
|
|
7
|
+
* settles when the lock is let go.
|
|
8
|
+
* @param {string} name
|
|
9
|
+
* @returns {Promise<{ release(): Promise<void> }>}
|
|
10
|
+
*/
|
|
11
|
+
export async function holdShared(name) {
|
|
12
|
+
const manager = locks();
|
|
13
|
+
if (!manager)
|
|
14
|
+
return { release: async () => { } };
|
|
15
|
+
let letGo;
|
|
16
|
+
const done = new Promise((resolve) => (letGo = resolve));
|
|
17
|
+
let held;
|
|
18
|
+
const acquired = new Promise((resolve) => (held = resolve));
|
|
19
|
+
const request = manager
|
|
20
|
+
.request(name, { mode: "shared" }, () => {
|
|
21
|
+
held();
|
|
22
|
+
return done;
|
|
23
|
+
})
|
|
24
|
+
.catch(() => held());
|
|
25
|
+
await acquired;
|
|
26
|
+
let released = null;
|
|
27
|
+
return {
|
|
28
|
+
release() {
|
|
29
|
+
if (!released) {
|
|
30
|
+
letGo();
|
|
31
|
+
released = request;
|
|
32
|
+
}
|
|
33
|
+
return released;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Runs `remove` holding the lock `name` exclusively, only if no page holds it, and resolves to
|
|
39
|
+
* whether it ran. Without Web Locks it runs.
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @param {() => Promise<void>} remove
|
|
42
|
+
* @returns {Promise<boolean>}
|
|
43
|
+
*/
|
|
44
|
+
export async function whenUnheld(name, remove) {
|
|
45
|
+
const manager = locks();
|
|
46
|
+
if (!manager) {
|
|
47
|
+
await remove();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return manager.request(name, { mode: "exclusive", ifAvailable: true }, async (lock) => {
|
|
51
|
+
if (!lock)
|
|
52
|
+
return false;
|
|
53
|
+
await remove();
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Runs `body` holding the lock `name` exclusively, waiting for any page that holds it, and resolves
|
|
59
|
+
* to what `body` resolves to; `body` is told whether it holds the lock. Where the browser has no
|
|
60
|
+
* Web Locks, or refuses the lock, `body` runs at once, holding nothing. Rejects with
|
|
61
|
+
* `signal.reason` once `signal` aborts while it waits.
|
|
62
|
+
* @template T
|
|
63
|
+
* @param {string} name
|
|
64
|
+
* @param {{ signal?: AbortSignal }} options
|
|
65
|
+
* @param {(held: boolean) => Promise<T>} body
|
|
66
|
+
* @returns {Promise<T>}
|
|
67
|
+
*/
|
|
68
|
+
export async function withExclusive(name, { signal } = {}, body) {
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
const manager = locks();
|
|
71
|
+
if (!manager)
|
|
72
|
+
return body(false);
|
|
73
|
+
let started = false;
|
|
74
|
+
try {
|
|
75
|
+
return await manager.request(name, signal ? { mode: "exclusive", signal } : { mode: "exclusive" }, () => {
|
|
76
|
+
started = true;
|
|
77
|
+
return body(true);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (started)
|
|
82
|
+
throw error;
|
|
83
|
+
signal?.throwIfAborted();
|
|
84
|
+
return body(false);
|
|
85
|
+
}
|
|
86
|
+
}
|