DEV Community

Cover image for How to darken a canvas-based editor without changing the document
Manoj
Manoj

Posted on AI-assisted

How to darken a canvas-based editor without changing the document

This article was prepared with AI assistance and checked against DarkWave v1.4.0's source and release tests.

Dark mode becomes difficult when the important pixels are not HTML. Google Sheets, Docs, Slides, and Drawings use canvas-backed or SVG-rendered surfaces. A stylesheet that changes element colors can miss canvas pixels completely. A filter on the page root reaches those pixels, but it also reaches photos, videos, icons, and other images.

The goal is a presentation change that leaves document content and saved formatting alone in the tested editor paths.

Use a presentation filter, not a color rewrite

DarkWave v1.4.0 applies an extension-owned stylesheet to the page. Its main operation is a filter on the root element:

html[data-darkwave] {
  filter: invert(1) hue-rotate(180deg)
          brightness(var(--darkwave-brightness, 0.85))
          contrast(var(--darkwave-contrast, 0.9)) !important;
}
Enter fullscreen mode Exit fullscreen mode

This does not rewrite the page's authored color or background-color declarations. On ordinary sites, DarkWave separately identifies image elements and genuine CSS background images, then applies a second filter where appropriate. It also avoids protecting page-scale containers, readable text containers, and ancestors of canvas content, because re-inverting those elements would cancel the page-level darkening.

With Protect Images enabled, ordinary images, videos, and SVGs receive only:

const CORRECTION = 'hue-rotate(180deg) invert(1)';
Enter fullscreen mode Exit fullscreen mode

That correction cancels the root filter's inversion and hue rotation. The page's brightness and contrast still apply once, so protected images remain close to their original appearance rather than matching every source pixel exactly. If image protection is disabled, images intentionally follow the page inversion.

Correct the editor at its drawing boundary

Google editor pages receive a separate adapter from darkwave-canvas.js. In v1.4.0, the manifest loads that file in the page's MAIN world only for Google editor URLs. The adapter wraps drawImage() on 2D canvas contexts, including transferred OffscreenCanvas contexts.

It does not treat every draw as a photograph. It recognizes image-like sources such as HTMLImageElement, SVGImageElement, ImageBitmap, video, and VideoFrame, then requires the destination canvas to remain connected to the document. A draw at least 40 CSS pixels wide and high qualifies as a picture. Smaller draws qualify only when they are at least 8 pixels on each side and the selected source region is at least 64 pixels on each side. Canvas sources, detached surfaces, and small glyph-like draws remain native.

The correction is applied by temporarily combining the editor's existing canvas filter with the inverse presentation filter:

function drawProtected(nativeDrawImage, context, source, args, protect) {
  const previous = context.filter;

  try {
    if (protect) {
      context.filter = previous && previous !== 'none'
        ? `${previous} ${CORRECTION}`
        : CORRECTION;
    }

    return nativeDrawImage.call(context, source, ...args);
  } finally {
    context.filter = previous;
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a simplified version of the v1.4 hot path; the source-size and connected-canvas checks are omitted.

Repaint without restoring stale pixels

Editors redraw constantly. DarkWave records qualifying image draws so it can repaint photos already visible when the user toggles dark mode. It keeps a bounded set of recent draws per canvas and replaces an older record when the same source is drawn in the same transformed location.

Before repainting, the adapter checks whether the recorded image still appears in the expected region. It copies the region to a private scratch canvas configured with willReadFrequently, then reads the scratch canvas. It never calls getImageData() directly on the editor's canvas.

The comparison uses the middle portion of the image and averages pixels in up to a 3×3 grid. That tolerates resampling, especially when Sheets shrinks a large source image into a small cell. The check is approximate rather than proof of identity. If the sampled region no longer resembles the recorded draw, the adapter skips the repaint instead of restoring an overwritten or deleted image.

The context-state description also needs precision. For a normal drawImage() call, the adapter changes and restores only ctx.filter. During a toggle repaint, it uses save() and restore() around temporary transform, clipping, alpha, composite, and filter changes. It should not be described as snapshotting every possible canvas detail: the repaint constructs a path with beginPath(), and Canvas state restoration does not restore the current path.

What “without changing the document” means

This is a data-integrity claim, not a document-security guarantee. The extension does not call Google's document APIs, rewrite cell or paragraph content, or replace authored color declarations. It does add an extension-owned style element and data attributes to the page, and its Google adapter patches drawImage() in the page's MAIN world.

The v1.4 test suite checks the narrower claim directly. Integrity tests compare computed colors, copied HTML, form values, editable content, and real ProseMirror and CodeMirror editors with DarkWave enabled and disabled. The live release checks type into Google Sheets and Docs, verify that editing succeeds, and confirm that the saved document remains usable without the extension. The tests also compare visible pictures against the same documents opened without DarkWave.

This article describes v1.4.0 specifically. Current v1.5.0 keeps the filter engine and the canvas adapter, but reorganizes settings, native-dark detection, and tagging into shared modules. Its service worker no longer applies page styles directly; content scripts react to storage changes instead.

The result is a dark presentation layer for canvas-driven editors while keeping the editor's data path separate. Try DarkWave from the official landing page.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

Official Platform Update

Security protocols have been updated for all developer accounts.

  • tr.ee/dev-to