Skip to content

Commit

Permalink
add double-click-mode attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
Christoph Nölle committed Mar 29, 2022
1 parent bbde9c1 commit 3e47f65
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 9 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ The `canvas` attributes `width` and `height` are supported, and furthermore:
* **max-zoom** (property: **maxZoom**): A positive number, typically > 1, representing the maximum scale value. Example: 8. Default value: `undefined`
* **min-zoom** (property: **minZoom**): A positive number, typically <= 1, the minimum scale value. Example: 0.125. Default value: `undefined`
* **zoom-factor** (property: **zoomFactor**): A number > 1 that determines the zoom velocity. Default value: 2.
* **double-click-mode** (property: **doubleClickMode**): Either "reset" (reset zoom and pan state on doubleclick) or "zoom" (zoom in on doubleclick, or zoom out if ctrl key is pressed at the same time) or absent (default, no action).


**Example**
```html
<canvas2d-zoom width="640" height="480" pan="false" zoom="true" max-zoom="8" min-zoom="0.125"></canvas2d-zoom>
<canvas2d-zoom width="640" height="480" pan="false" zoom="true" max-zoom="8" min-zoom="0.125" double-click-mode="reset"></canvas2d-zoom>
```

## Interactions
Expand All @@ -82,12 +83,16 @@ Currently only mouse and keyboard interactions are supported (no mobile gestures

**Zoom**
* Mouse wheel
* *Ctrl* + '+' (zoom in) or *Ctrl* + '-' (zoom out) (requires focus on canvas, e.g. by clicking the canvas once)
* *Ctrl* + '+' (zoom in) or *Ctrl* + '-' (zoom out) (requires focus on canvas, e.g. by clicking the canvas once)

**Pan**
* Mouse drag
* *Ctrl* + keyboard arrow buttons (requires focus on canvas, e.g. by clicking the canvas once)

**Reset zoom and pan**
* Mouse double click, if attribute *double-click-mode* is set to *reset*
* *Ctrl* + *Enter* (requires focus on canvas, e.g. by clicking the canvas once)

## How it works

The element remembers all calls to the [CanvasRenderingContext2D](https://developer.mozilla.org/de/docs/Web/API/CanvasRenderingContext2D) methods relevant to drawing, such as `ctx.beginPath()`, `ctx.rect()` or `ctx.stroke()`. When the user pans or zooms (via mouse or keyboard interactions), the canvas is cleared, a transformation appropriate for the zoom and pan state is set, and the canvas is redrawn.
Expand Down
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export declare class Canvas2dZoom extends HTMLElement {
* the visible range shall not be cleared (for negative values). Default: 0.
*/
overlapY: number;
/**
* Define action on double click of the user.
* "reset": reset zoom and pan
* "zoom": zoom in, or out if ctrl key is pressed at the same time
* null (default): no action
*/
doubleClickMode: "reset"|"zoom"|null;
/**
* Returns an object that provides methods and properties for drawing and manipulating images and graphics on a
* 2D canvas element in a document. A context object includes information about colors, line widths, fonts, and
Expand Down
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<body>
<h1>Canvas Zoom demo app</h1>
<div style="display: flex; flex-wrap: wrap; column-gap: 1em;">
<canvas2d-zoom width="640" height="480" min-zoom="0.25" max-zoom="8"></canvas2d-zoom>
<canvas2d-zoom width="640" height="480" min-zoom="0.25" max-zoom="8" double-click-mode="reset"></canvas2d-zoom>
<div><input type="button" value="Reset" id="reset"></div>
</div>
<script>
Expand Down Expand Up @@ -77,7 +77,7 @@ <h1>Canvas Zoom demo app</h1>
// Here we use the latest published version from npmjs via unpkg"
// For local development replace the url by "./bundle.js" and run `npm run start`.
// This will start a webpack dev server that generates the file bundle.js from src/canvas2d-zoom.ts
import { Canvas2dZoom } from "https://unpkg.com/canvas2d-zoom@latest/dist/canvas2d-zoom.js";
import { Canvas2dZoom } from "https://unpkg.com/canvas2d-zoom@latest/dist/canvas2d-zoom.js";
Canvas2dZoom.register();
</script>
</body>
22 changes: 21 additions & 1 deletion src/MouseEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Point, MouseEventListener } from "./internalTypes.js";
import { Point, MouseEventListener, DoubleClickMode } from "./internalTypes.js";

/**
* Supports two modes: zoom/pan
Expand All @@ -17,6 +17,7 @@ export class MouseEventHandler {
#panMode: boolean = false;
#downAnchor: Point|null = null;
#lastSeen: Point|null = null;
#doubleClickMode: DoubleClickMode = null;

private _removeMouseListeners() {
this.element.removeEventListener("pointermove", this.#mouseMoveListener);
Expand Down Expand Up @@ -88,7 +89,26 @@ export class MouseEventHandler {
this.#mouseUpListener = this.#mouseUp.bind(this);
this.#mouseMoveListener = this.#mouseMove.bind(this);
element.addEventListener("pointerdown", this.#mouseDown.bind(this));
element.addEventListener("dblclick", event => {
if (this.#doubleClickMode === null)
return;
switch (this.#doubleClickMode) {
case "reset":
listener.reset();
break;
case "zoom":
listener.zoomed(!event.ctrlKey, {x: event.offsetX, y: event.offsetY});
break;
}
});
}

setDoubleClickMode(mode: DoubleClickMode) {
this.#doubleClickMode = mode;
}

getDoubleClickMode(): DoubleClickMode {
return this.#doubleClickMode;
}

}
36 changes: 32 additions & 4 deletions src/canvas2d-zoom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ContextProxy } from "./ContextProxy.js";
import { MouseEventHandler } from "./MouseEventHandler.js";
import { MouseEventListener, Point } from "./internalTypes.js";
import { MouseEventListener, Point, DoubleClickMode } from "./internalTypes.js";

/**
* A webcomponent that wraps a 2D HTML canvas element, making it zoomable and pannable.
Expand All @@ -21,7 +21,7 @@ export class Canvas2dZoom extends HTMLElement {
private static _tag: string|undefined;

static get observedAttributes() {
return ["debug", "width", "height", "zoom", "pan", "max-zoom", "min-zoom", "zoom-factor"];
return ["debug", "width", "height", "zoom", "pan", "max-zoom", "min-zoom", "zoom-factor", "double-click-mode"];
}

/**
Expand Down Expand Up @@ -63,12 +63,15 @@ export class Canvas2dZoom extends HTMLElement {
this.#canvas.addEventListener("wheel", this.#noopZoomListener);
const keyListener = (event: KeyboardEvent) => {
const isZoom: boolean = event.key === "+" || event.key === "-";
const isZoomReset: boolean = event.key === "Enter";
const isTranslation: boolean = event.key.startsWith("Arrow");
if (event.ctrlKey && ((isZoom && this.#zoom) || (isTranslation && this.#pan))) {
if (event.ctrlKey && (((isZoom || isZoomReset) && this.#zoom) || (isTranslation && this.#pan))) {
event.preventDefault();
if (isZoom) {
const factor: number = this.#proxy.getZoomFactor();
this.applyZoom(event.key === "+" ? factor : 1/factor, this.#lastFocusPoint);
} else if (isZoomReset) {
this.resetZoomPan();
} else if (isTranslation) {
// TODO configurable translation step?
const vector: Point = event.key === "ArrowUp" ? {x: 0, y: 10} :
Expand Down Expand Up @@ -108,7 +111,10 @@ export class Canvas2dZoom extends HTMLElement {
if (frac >= 1)
return;
this.applyZoom(1/frac, { x: midx, y: midy });
}
},
reset: () => this.resetZoomPan(),
zoomed: (inOrOut: boolean, center: DOMPointInit) => this.applyZoom(inOrOut ? 2 : 0.5, center)

};
const style: HTMLStyleElement = document.createElement("style");
style.textContent = ":host { position: relative; display: block; }";
Expand Down Expand Up @@ -254,6 +260,23 @@ export class Canvas2dZoom extends HTMLElement {
this.setAttribute("overlap-y", pixels+ "");
}

get doubleClickMode(): "reset"|"zoom"|null {
return this.#mouseHandler.getDoubleClickMode();
}

/**
* Define action on double click of the user.
* "reset" => reset zoom and pan
* "zoom" => zoom in (or out if ctrl key is pressed at the same time)
* null (default) => no action
*/
set doubleClickMode(mode: "reset"|"zoom"|null) {
if (!mode)
this.removeAttribute("double-click-mode");
else
this.setAttribute("double-click-mode", mode);
}

// ============= Internal methods ===============

connectedCallback() {
Expand Down Expand Up @@ -289,6 +312,11 @@ export class Canvas2dZoom extends HTMLElement {
if (factor > 0 && isFinite(factor))
this.#proxy.setZoomFactor(factor);
break;
case "double-click-mode":
const dcm: string = newValue?.toLowerCase();
const dcmValue: DoubleClickMode = dcm === "reset" || dcm === "zoom" ? dcm : null;
this.#mouseHandler.setDoubleClickMode(dcmValue);
break;
case "debug":
const debug: boolean = newValue?.toLowerCase() === "true";
if (debug) {
Expand Down
5 changes: 5 additions & 0 deletions src/internalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ export interface MouseEventListener {

moved: (vector: Point) => void;
selected: (topLeft: Point, bottomRight: Point, panMode: boolean) => void;
reset: () => void;
zoomed: (inOrOut: boolean, center: DOMPointInit) => void;

}

export type DoubleClickMode = "reset"|"zoom"|null;

0 comments on commit 3e47f65

Please sign in to comment.