@leviosa-ai/detail-page-editor 0.6.2 → 0.6.3

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.
@@ -45,7 +45,7 @@ export const MAX_GIF_WIDTH = 512;
45
45
  /**
46
46
  * MP4 gets a wider cap than GIF/WebP. That 512 is a *transfer* limit: GIF pays
47
47
  * for every pixel in a palette-coded frame, and WebP frames travel to the server
48
- * as PNG under an upload cap. MP4 is encoded right here by a hardware H.264
48
+ * one by one under an upload cap. MP4 is encoded right here by a hardware H.264
49
49
  * encoder that eats resolution cheaply, so holding it at 512 would only make the
50
50
  * video soft on a ~860px-wide detail page for no gain.
51
51
  */
@@ -131,12 +131,35 @@ function buildGlobalPalette(frames: HTMLCanvasElement[]): number[][] {
131
131
  /** Output format for an animated section. WebP is the default. */
132
132
  export type AnimationFormat = "webp" | "gif" | "mp4";
133
133
 
134
- /** Turn a canvas into a PNG Blob — the wire format for server-side encoding. */
135
- function canvasToPngBlob(canvas: HTMLCanvasElement): Promise<Blob> {
134
+ /**
135
+ * Wire format for the frames that travel to the server for WebP encoding.
136
+ *
137
+ * They used to go as lossless PNG. That is what a section costs when nothing is
138
+ * thrown away: a 512×2644 composed frame is ~1.4MB as PNG, and a section is up
139
+ * to ``GIF_MAX_FRAMES`` of them — ~56MB of request body, which no edge in front
140
+ * of the API accepts (nginx answers 413 before the app sees a byte).
141
+ *
142
+ * The same frame is ~220KB as quality-92 WebP. Nothing visible is lost on the
143
+ * way: the server's animated WebP is itself encoded at quality 80, so a
144
+ * near-lossless intermediate is already below the floor the output sits on.
145
+ */
146
+ const FRAME_WIRE_TYPE = "image/webp";
147
+ const FRAME_WIRE_QUALITY = 0.92;
148
+
149
+ /**
150
+ * Turn a canvas into the Blob that goes over the wire.
151
+ *
152
+ * A browser that cannot encode WebP falls back to PNG on its own — ``toBlob``
153
+ * is specified to use ``image/png`` for a type it does not support — and the
154
+ * server sniffs the bytes rather than trusting the name, so that path still
155
+ * encodes. It just pays the old size.
156
+ */
157
+ export function canvasToFrameBlob(canvas: HTMLCanvasElement): Promise<Blob> {
136
158
  return new Promise((resolve, reject) => {
137
159
  canvas.toBlob(
138
160
  (blob) => (blob ? resolve(blob) : reject(new Error("canvas toBlob failed"))),
139
- "image/png",
161
+ FRAME_WIRE_TYPE,
162
+ FRAME_WIRE_QUALITY,
140
163
  );
141
164
  });
142
165
  }
@@ -161,8 +184,8 @@ function encodeFramesAsGif(frames: HTMLCanvasElement[], delayMs: number): Blob {
161
184
  * Encode composed frames as an animated WebP.
162
185
  *
163
186
  * The round trip is not an optimisation choice — no browser can encode animated
164
- * WebP, so the frames have to leave the page. They go as lossless PNG so the
165
- * server compresses the original pixels once rather than compounding losses.
187
+ * WebP, so the frames have to leave the page. See ``FRAME_WIRE_TYPE`` for why
188
+ * they leave as WebP rather than as the PNG they are composed in.
166
189
  */
167
190
  async function encodeFramesAsWebp(
168
191
  host: DetailPageHost,
@@ -170,7 +193,7 @@ async function encodeFramesAsWebp(
170
193
  delayMs: number,
171
194
  signal?: AbortSignal,
172
195
  ): Promise<Blob> {
173
- const payload = await Promise.all(frames.map(canvasToPngBlob));
196
+ const payload = await Promise.all(frames.map(canvasToFrameBlob));
174
197
  const fps = Math.max(1, Math.round(1000 / Math.max(1, delayMs)));
175
198
  return host.api.encodeDetailPageAnimation(payload, { fps, format: "webp" }, signal);
176
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviosa-ai/detail-page-editor",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Leviosa detail-page editor shell. Source-only: consumers compile it through Next transpilePackages. Every app coupling (sourcing server, brand store, cache keys, toast) arrives through the DetailPageHost interface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",