@jackuait/blok 0.18.0 → 0.19.1
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/dist/blok.cjs +1 -1
- package/dist/blok.iife.js +4 -4
- package/dist/blok.mjs +2 -2
- package/dist/blok.umd.js +3 -3
- package/dist/chunks/{blok-BoAuTqli.mjs → blok-C52qeiPg.mjs} +31 -8
- package/dist/chunks/{blok-B7IRrvgX.cjs → blok-gFn3FI95.cjs} +3 -3
- package/dist/chunks/{constants-BWoyU4yv.cjs → constants-DmKwEMyC.cjs} +1 -1
- package/dist/chunks/{constants-BO6zLfL2.mjs → constants-DzQOOvgv.mjs} +2 -1
- package/dist/chunks/{tools-DXagFf6I.cjs → tools-BcF1rHBg.cjs} +2 -2
- package/dist/chunks/{tools-DRwLS-6t.mjs → tools-CelSzJoA.mjs} +384 -285
- package/dist/full.cjs +1 -1
- package/dist/full.mjs +3 -3
- package/dist/react.cjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/blok.ts +50 -4
- package/src/components/constants/data-attributes.ts +2 -0
- package/src/components/modules/ui.ts +30 -0
- package/src/styles/audio.css +26 -3
- package/src/styles/main.css +11 -0
- package/src/tools/audio/index.ts +76 -21
- package/src/tools/audio/liveliness.ts +136 -0
- package/src/tools/audio/ui.ts +6 -1
- package/src/tools/audio/waveform.ts +130 -24
- package/types/api/index.d.ts +1 -0
- package/types/api/width.d.ts +31 -0
- package/types/data-attributes.d.ts +1 -0
- package/types/index.d.ts +32 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WAVEFORM_BUCKETS } from './constants';
|
|
2
|
+
import { liveAmplitude, headColorBlend, entranceEase } from './liveliness';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Reduce raw mono samples to `buckets` peak values normalized to 0..1.
|
|
@@ -95,16 +96,51 @@ export function attachWaveform(opts: {
|
|
|
95
96
|
canvas.className = 'blok-audio-waveform__canvas';
|
|
96
97
|
mount.appendChild(canvas);
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const prefersReducedMotion = (): boolean =>
|
|
100
|
+
globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
101
|
+
|
|
102
|
+
// Animation state. `playing` is true between play and pause/ended and pins the
|
|
103
|
+
// boost energy at full. On pause we keep the loop alive briefly with `settling`
|
|
104
|
+
// so the bars glide back to their resting peaks (energy ramps 1→0) instead of
|
|
105
|
+
// snapping. `energy` reflects the current boost multiplier.
|
|
106
|
+
const SETTLE_MS = 420;
|
|
107
|
+
// The comet-head colour blooms in over ENTRANCE_MS each time playback starts.
|
|
108
|
+
const ENTRANCE_MS = 650;
|
|
109
|
+
const anim = { playing: false, settling: false, settleStart: 0, entranceStart: 0, rafId: 0 };
|
|
110
|
+
|
|
111
|
+
const energyAt = (now: number): number => {
|
|
112
|
+
if (anim.playing) return 1;
|
|
113
|
+
if (!anim.settling) return 0;
|
|
114
|
+
return Math.min(1, Math.max(0, 1 - (now - anim.settleStart) / SETTLE_MS));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const entranceAt = (now: number): number => entranceEase((now - anim.entranceStart) / ENTRANCE_MS);
|
|
118
|
+
|
|
119
|
+
const draw = (now: number): void => {
|
|
99
120
|
const rect = canvas.getBoundingClientRect();
|
|
100
121
|
if (rect.width <= 0 || rect.height <= 0) return;
|
|
101
122
|
const dpr = globalThis.devicePixelRatio || 1;
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
const targetW = Math.round(rect.width * dpr);
|
|
124
|
+
const targetH = Math.round(rect.height * dpr);
|
|
125
|
+
// Resizing the backing store clears it; only pay that cost when the box
|
|
126
|
+
// actually changed (every frame of the loop would otherwise reallocate).
|
|
127
|
+
if (canvas.width !== targetW || canvas.height !== targetH) {
|
|
128
|
+
canvas.width = targetW;
|
|
129
|
+
canvas.height = targetH;
|
|
130
|
+
}
|
|
104
131
|
const ctx = canvas.getContext('2d');
|
|
105
132
|
if (!ctx) return;
|
|
106
|
-
ctx.
|
|
133
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
134
|
+
ctx.clearRect(0, 0, rect.width, rect.height);
|
|
135
|
+
|
|
107
136
|
const played = media.duration ? media.currentTime / media.duration : 0;
|
|
137
|
+
const playheadIndex = played * peaks.length;
|
|
138
|
+
const reduced = prefersReducedMotion();
|
|
139
|
+
const timeSeconds = now / 1000;
|
|
140
|
+
const energy = reduced ? 0 : energyAt(now);
|
|
141
|
+
const live = energy > 0;
|
|
142
|
+
const entrance = live ? entranceAt(now) : 0;
|
|
143
|
+
|
|
108
144
|
const slot = rect.width / peaks.length;
|
|
109
145
|
const gap = Math.min(2, slot * 0.34);
|
|
110
146
|
const barW = Math.max(1, slot - gap);
|
|
@@ -112,11 +148,9 @@ export function attachWaveform(opts: {
|
|
|
112
148
|
const styles = getComputedStyle(canvas);
|
|
113
149
|
const playedColor = styles.getPropertyValue('--blok-audio-bar-played').trim() || '#222';
|
|
114
150
|
const baseColor = styles.getPropertyValue('--blok-audio-bar').trim() || '#ccc';
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const y = (rect.height - h) / 2;
|
|
119
|
-
ctx.fillStyle = i / peaks.length < played ? playedColor : baseColor;
|
|
151
|
+
const headColor = styles.getPropertyValue('--blok-audio-bar-head').trim() || playedColor;
|
|
152
|
+
|
|
153
|
+
const paintBar = (x: number, y: number, h: number): void => {
|
|
120
154
|
if (typeof ctx.roundRect === 'function') {
|
|
121
155
|
ctx.beginPath();
|
|
122
156
|
ctx.roundRect(x, y, barW, h, radius);
|
|
@@ -124,26 +158,91 @@ export function attachWaveform(opts: {
|
|
|
124
158
|
} else {
|
|
125
159
|
ctx.fillRect(x, y, barW, h);
|
|
126
160
|
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
peaks.forEach((peak, i) => {
|
|
164
|
+
const amp = live
|
|
165
|
+
? liveAmplitude({ basePeak: peak, index: i, playheadIndex, timeSeconds, reduced, energy })
|
|
166
|
+
: peak;
|
|
167
|
+
const h = Math.max(2, amp * rect.height * 0.92);
|
|
168
|
+
const x = i * slot;
|
|
169
|
+
const y = (rect.height - h) / 2;
|
|
170
|
+
ctx.fillStyle = i < playheadIndex ? playedColor : baseColor;
|
|
171
|
+
paintBar(x, y, h);
|
|
172
|
+
|
|
173
|
+
// Comet-head colour shift — overlay the head tint, its alpha ramping in
|
|
174
|
+
// with the entrance and fading out on the settle (energy).
|
|
175
|
+
if (live) {
|
|
176
|
+
const blend = headColorBlend({ distance: i - playheadIndex, energy, entrance });
|
|
177
|
+
if (blend > 0.001) {
|
|
178
|
+
ctx.save();
|
|
179
|
+
ctx.globalAlpha = Math.min(1, blend);
|
|
180
|
+
ctx.fillStyle = headColor;
|
|
181
|
+
paintBar(x, y, h);
|
|
182
|
+
ctx.restore();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
127
185
|
});
|
|
128
186
|
};
|
|
129
187
|
|
|
130
|
-
const
|
|
188
|
+
const setSeekVar = (): void => {
|
|
189
|
+
mount.style.setProperty(
|
|
190
|
+
'--blok-audio-seek-pct',
|
|
191
|
+
String(media.duration ? (media.currentTime / media.duration) * 100 : 0),
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const loop = (now: number): void => {
|
|
196
|
+
draw(now);
|
|
197
|
+
// Keep looping while playing, or while the post-pause settle still has energy.
|
|
198
|
+
const keepGoing = anim.playing || (anim.settling && energyAt(now) > 0);
|
|
199
|
+
if (keepGoing) {
|
|
200
|
+
anim.rafId = requestAnimationFrame(loop);
|
|
201
|
+
} else {
|
|
202
|
+
anim.settling = false;
|
|
203
|
+
anim.rafId = 0;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const onPlay = (): void => {
|
|
208
|
+
setSeekVar();
|
|
209
|
+
anim.playing = true;
|
|
210
|
+
anim.settling = false;
|
|
211
|
+
// Restart the head-colour entrance so it blooms in on every play.
|
|
212
|
+
anim.entranceStart = globalThis.performance?.now?.() ?? 0;
|
|
213
|
+
// No continuous loop under reduced motion — timeupdate alone advances the
|
|
214
|
+
// played boundary, with no dancing bars.
|
|
215
|
+
if (!anim.rafId && !prefersReducedMotion()) anim.rafId = requestAnimationFrame(loop);
|
|
216
|
+
};
|
|
217
|
+
const onStop = (): void => {
|
|
218
|
+
// Hand off to the settle ramp so the bars glide down rather than snap. Under
|
|
219
|
+
// reduced motion there's no loop/boost to settle, so just stop.
|
|
220
|
+
if (anim.playing && !prefersReducedMotion()) {
|
|
221
|
+
anim.playing = false;
|
|
222
|
+
anim.settling = true;
|
|
223
|
+
anim.settleStart = globalThis.performance?.now?.() ?? 0;
|
|
224
|
+
if (!anim.rafId) anim.rafId = requestAnimationFrame(loop);
|
|
225
|
+
} else {
|
|
226
|
+
anim.playing = false;
|
|
227
|
+
anim.settling = false;
|
|
228
|
+
if (anim.rafId) { cancelAnimationFrame(anim.rafId); anim.rafId = 0; }
|
|
229
|
+
draw(0);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Keeps the played boundary + seek var current while paused-seeking or when
|
|
234
|
+
// the continuous loop is off (reduced motion). The loop owns drawing while it
|
|
235
|
+
// runs, so skip the redundant paint then.
|
|
131
236
|
const onTime = (): void => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
rafState.id = 0;
|
|
135
|
-
mount.style.setProperty(
|
|
136
|
-
'--blok-audio-seek-pct',
|
|
137
|
-
String(media.duration ? (media.currentTime / media.duration) * 100 : 0),
|
|
138
|
-
);
|
|
139
|
-
draw();
|
|
140
|
-
});
|
|
237
|
+
setSeekVar();
|
|
238
|
+
if (!anim.rafId) draw(0);
|
|
141
239
|
};
|
|
142
240
|
|
|
143
241
|
const seek = (clientX: number): void => {
|
|
144
242
|
if (!media.duration) return;
|
|
145
243
|
media.currentTime = ratioFromPointer(clientX, canvas.getBoundingClientRect()) * media.duration;
|
|
146
|
-
|
|
244
|
+
setSeekVar();
|
|
245
|
+
if (!anim.rafId) draw(0);
|
|
147
246
|
};
|
|
148
247
|
const drag = { active: false };
|
|
149
248
|
const onDown = (e: PointerEvent): void => { drag.active = true; seek(e.clientX); };
|
|
@@ -153,18 +252,25 @@ export function attachWaveform(opts: {
|
|
|
153
252
|
canvas.addEventListener('pointerdown', onDown);
|
|
154
253
|
globalThis.addEventListener('pointermove', onMove);
|
|
155
254
|
globalThis.addEventListener('pointerup', onUp);
|
|
255
|
+
media.addEventListener('play', onPlay);
|
|
256
|
+
media.addEventListener('pause', onStop);
|
|
257
|
+
media.addEventListener('ended', onStop);
|
|
156
258
|
media.addEventListener('timeupdate', onTime);
|
|
157
|
-
media.addEventListener('loadedmetadata',
|
|
158
|
-
draw();
|
|
259
|
+
media.addEventListener('loadedmetadata', onTime);
|
|
260
|
+
draw(0);
|
|
159
261
|
|
|
160
262
|
return {
|
|
161
263
|
destroy(): void {
|
|
162
|
-
if (
|
|
264
|
+
if (anim.rafId) cancelAnimationFrame(anim.rafId);
|
|
265
|
+
anim.playing = false;
|
|
163
266
|
canvas.removeEventListener('pointerdown', onDown);
|
|
164
267
|
globalThis.removeEventListener('pointermove', onMove);
|
|
165
268
|
globalThis.removeEventListener('pointerup', onUp);
|
|
269
|
+
media.removeEventListener('play', onPlay);
|
|
270
|
+
media.removeEventListener('pause', onStop);
|
|
271
|
+
media.removeEventListener('ended', onStop);
|
|
166
272
|
media.removeEventListener('timeupdate', onTime);
|
|
167
|
-
media.removeEventListener('loadedmetadata',
|
|
273
|
+
media.removeEventListener('loadedmetadata', onTime);
|
|
168
274
|
canvas.remove();
|
|
169
275
|
},
|
|
170
276
|
};
|
package/types/api/index.d.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor content width mode.
|
|
3
|
+
*
|
|
4
|
+
* - `'narrow'` — content is constrained to the default `--max-width-content` (the default).
|
|
5
|
+
* - `'full'` — content max-width constraint is removed so it fills its container.
|
|
6
|
+
*/
|
|
7
|
+
export type EditorWidth = 'full' | 'narrow';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes Blok's width API for controlling the editor content width mode.
|
|
11
|
+
*/
|
|
12
|
+
export interface Width {
|
|
13
|
+
/**
|
|
14
|
+
* Returns the current editor content width mode.
|
|
15
|
+
*
|
|
16
|
+
* @returns {EditorWidth} the active mode (defaults to `'narrow'`)
|
|
17
|
+
*/
|
|
18
|
+
get(): EditorWidth;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets the editor content width mode.
|
|
22
|
+
*
|
|
23
|
+
* @param {EditorWidth} value - the mode to apply
|
|
24
|
+
*/
|
|
25
|
+
set(value: EditorWidth): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Toggles the editor content width mode between `'narrow'` and `'full'`.
|
|
29
|
+
*/
|
|
30
|
+
toggle(): void;
|
|
31
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
Theme,
|
|
34
34
|
ThemeMode,
|
|
35
35
|
ResolvedTheme,
|
|
36
|
+
Width,
|
|
36
37
|
} from './api';
|
|
37
38
|
|
|
38
39
|
import { OutputData } from './data-formats';
|
|
@@ -129,6 +130,8 @@ export {
|
|
|
129
130
|
Theme,
|
|
130
131
|
ThemeMode,
|
|
131
132
|
ResolvedTheme,
|
|
133
|
+
Width,
|
|
134
|
+
EditorWidth,
|
|
132
135
|
} from './api';
|
|
133
136
|
export {
|
|
134
137
|
BlockMutationType,
|
|
@@ -204,14 +207,41 @@ export function wrapLegacyInlineTool(
|
|
|
204
207
|
}
|
|
205
208
|
): InlineToolConstructable;
|
|
206
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The surface of a Blok instance that is guaranteed to exist synchronously,
|
|
212
|
+
* immediately after `new Blok()` and before `isReady` has resolved.
|
|
213
|
+
*
|
|
214
|
+
* Blok constructs its module APIs (`blocks`, `caret`, `history`, `readOnly`, …)
|
|
215
|
+
* asynchronously: they only become available once `isReady` resolves. Accessing
|
|
216
|
+
* them earlier returns `undefined` at runtime. Type a reference you hold during
|
|
217
|
+
* that window as `PendingBlok` so the not-yet-available members are unreachable,
|
|
218
|
+
* then await `isReady` to obtain the fully-initialized {@link Blok}:
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* const pending: PendingBlok = new Blok(config);
|
|
222
|
+
* const editor = await pending.isReady; // editor is a ready Blok
|
|
223
|
+
* editor.blocks.render(data);
|
|
224
|
+
*/
|
|
225
|
+
export interface PendingBlok {
|
|
226
|
+
/** Resolves with the fully-initialized Blok instance once core modules are ready. */
|
|
227
|
+
isReady: Promise<Blok>;
|
|
228
|
+
/** Destroy the instance. Safe to call before `isReady` resolves. */
|
|
229
|
+
destroy(): void;
|
|
230
|
+
/** Theme API, exposed immediately after construction. */
|
|
231
|
+
theme: Theme;
|
|
232
|
+
/** Width API, exposed immediately after construction. */
|
|
233
|
+
width: Width;
|
|
234
|
+
}
|
|
235
|
+
|
|
207
236
|
/**
|
|
208
237
|
* Main Blok class
|
|
209
238
|
*/
|
|
210
239
|
export class Blok {
|
|
211
|
-
public isReady: Promise<
|
|
240
|
+
public isReady: Promise<Blok>;
|
|
212
241
|
|
|
213
242
|
public blocks: Blocks;
|
|
214
243
|
public caret: Caret;
|
|
244
|
+
public history: History;
|
|
215
245
|
public sanitizer: Sanitizer;
|
|
216
246
|
public saver: Saver;
|
|
217
247
|
public selection: Selection;
|
|
@@ -221,6 +251,7 @@ export class Blok {
|
|
|
221
251
|
public tooltip: Tooltip;
|
|
222
252
|
public readOnly: ReadOnly;
|
|
223
253
|
public theme: Theme;
|
|
254
|
+
public width: Width;
|
|
224
255
|
constructor(configuration?: BlokConfig|string);
|
|
225
256
|
|
|
226
257
|
/**
|