Give a background something to cover with

- The stand-in backgrounds were the app's own gradients, which are overlay
  scrims: measured, not one pixel in either is opaque, and where they look
  dark they are transparent. As a background their dark half was simply
  absent, which is what showed on the slow path.
- They are now laid on the canvas colour they are painted over and kept as
  JPEG, which cannot carry transparency at all.
- The same hole was waiting for anyone's own picture: a canvas starts
  transparent, WebP keeps an alpha channel, and a file small enough to keep
  as it was never passed through either. Every picture now goes through an
  opaque ground, whatever its size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-09-18 13:59:07 +02:00
co-authored by Claude Opus 5
parent bfc42d8d7f
commit dfd42e2edd
4 changed files with 24 additions and 9 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+13 -6
View File
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import desktopGradient from "../graphics/desktop-gradient.png?url";
import mobileGradient from "../graphics/mobile-gradient.png?url";
import indoorStandIn from "../graphics/background-indoor-standin.jpg?url";
import outdoorStandIn from "../graphics/background-outdoor-standin.jpg?url";
/** How much to blur, when the chosen effect is blur. */
export const blurRadius = 15;
@@ -17,11 +17,18 @@ export interface ShippedBackground {
}
// EXPLORATION SHORTCUT (S1): the two shipped backgrounds, one indoor and one
// outdoor, do not exist yet. These stand-ins are existing gradient assets, so
// the pipeline can be exercised and measured. Never port this.
// outdoor, do not exist yet. These stand-ins are the app's own gradients, laid
// on the canvas colour they are painted over and kept as JPEG.
//
// Not a detail: those gradients are overlay scrims, and measured, not one
// pixel in either is opaque — where they look dark they are transparent. Used
// directly as a background their dark half was simply absent, which is what
// showed on the slow path. Flattening them is what makes them a picture rather
// than a veil, and JPEG cannot carry transparency at all, so it cannot come
// back. Never port this.
export const shippedBackgrounds: ShippedBackground[] = [
{ id: "indoor", imagePath: desktopGradient },
{ id: "outdoor", imagePath: mobileGradient },
{ id: "indoor", imagePath: indoorStandIn },
{ id: "outdoor", imagePath: outdoorStandIn },
];
export type BackgroundEffect =
+11 -3
View File
@@ -83,16 +83,24 @@ export async function prepareImage(file: Blob): Promise<Blob> {
}
try {
// Every picture goes through the canvas, not only the oversized ones: a
// background covers what is behind it, so it has to be opaque, and a
// picture with transparency in it — a logo, a screenshot with rounded
// corners — would otherwise be stored with its holes and drawn with them.
// Passing small files straight through is what kept them.
const longest = Math.max(bitmap.width, bitmap.height);
if (longest <= maxStoredEdge) return file;
const scale = maxStoredEdge / longest;
const scale = Math.min(1, maxStoredEdge / longest);
const canvas = new OffscreenCanvas(
Math.round(bitmap.width * scale),
Math.round(bitmap.height * scale),
);
const context = canvas.getContext("2d");
if (!context) throw new UnusableImage("undecodable");
// The ground the picture is laid on, so nothing it does not cover is a
// hole. Black rather than white: an unfilled corner reads as the frame's
// own edge rather than as a lamp.
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
return await canvas.convertToBlob({ type: "image/webp", quality: 0.9 });
} finally {