scenepic/tssrc/Canvas2D.ts

817 строки
30 KiB
TypeScript

import Misc from "./Misc"
import { ObjectCache, CanvasBase } from "./CanvasBase"
import { mat3, vec2 } from "gl-matrix";
abstract class Primitive {
readonly layerId: string;
constructor(layerId: string) {
this.layerId = layerId;
}
GetObjectCacheId(): string { return null; }
abstract Draw(HTMLCanvasElement, CanvasRenderingContext2D, ObjectCache);
}
class LinesPrimitive extends Primitive {
readonly strokeStyle: string;
readonly lineWidth: number;
readonly fillStyle: string;
readonly coordinates: Float32Array;
readonly closePath: boolean;
constructor(strokeStyle: string, lineWidth: number, fillStyle: string, coordinates: Float32Array, closePath: boolean, layerId: string) {
super(layerId);
this.strokeStyle = strokeStyle;
this.lineWidth = lineWidth;
this.fillStyle = fillStyle;
this.coordinates = coordinates;
this.closePath = closePath;
}
Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
context.beginPath();
context.moveTo(this.coordinates[0] * window.devicePixelRatio, this.coordinates[1] * window.devicePixelRatio);
for (var i = 0; i < this.coordinates.length; i += 2)
context.lineTo(this.coordinates[i] * window.devicePixelRatio, this.coordinates[i + 1] * window.devicePixelRatio);
if (this.closePath)
context.closePath();
if (this.fillStyle != null) {
context.fillStyle = this.fillStyle;
context.fill();
}
if (this.strokeStyle != null && this.lineWidth > 0) {
context.strokeStyle = this.strokeStyle;
context.lineWidth = this.lineWidth;
context.stroke();
}
}
}
class CirclePrimitive extends Primitive {
readonly strokeStyle: string;
readonly lineWidth: number;
readonly fillStyle: string;
readonly center: Float32Array;
readonly radius: number;
constructor(strokeStyle: string, lineWidth: number, fillStyle: string, center: Float32Array, radius: number, layerId: string) {
super(layerId);
this.strokeStyle = strokeStyle;
this.lineWidth = lineWidth;
this.fillStyle = fillStyle;
this.center = center;
this.radius = radius;
}
Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
context.beginPath();
context.arc(this.center[0] * window.devicePixelRatio, this.center[1] * window.devicePixelRatio, this.radius * window.devicePixelRatio, 0.0, 2.0 * Math.PI, false);
if (this.fillStyle != null) {
context.fillStyle = this.fillStyle;
context.fill();
}
if (this.strokeStyle != null && this.lineWidth > 0) {
context.strokeStyle = this.strokeStyle;
context.lineWidth = this.lineWidth;
context.stroke();
}
}
}
class TextPrimitive extends Primitive {
readonly text: string;
readonly fillStyle: string;
readonly sizeInPixels: number;
readonly fontFamily: string;
readonly position: Float32Array;
constructor(text: string, fillStyle: string, sizeInPixels: number, fontFamily: string, position: Float32Array, layerId: string) {
super(layerId);
this.text = text;
this.fillStyle = fillStyle;
this.sizeInPixels = sizeInPixels;
this.fontFamily = fontFamily;
this.position = position;
}
Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
context.fillStyle = this.fillStyle;
let sizeInPixels = this.sizeInPixels * window.devicePixelRatio;
let left = this.position[0] * window.devicePixelRatio;
let bottom = this.position[1] * window.devicePixelRatio;
let font = sizeInPixels.toString() + "px " + this.fontFamily;
context.font = font;
context.fillText(this.text, left, bottom);
}
}
class ImagePrimitive extends Primitive {
readonly imageId: string;
readonly positionType: string;
readonly position: Float32Array
readonly scale: number;
readonly smoothed: boolean;
constructor(imageId: string, positionType: string, position: Float32Array, scale: number, smoothed: boolean, layerId: string) {
super(layerId);
this.imageId = imageId;
this.positionType = positionType;
this.position = position;
this.scale = scale;
this.smoothed = smoothed;
}
GetObjectCacheId() {
return this.imageId;
}
Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
// Get current image
var currentImage = objectCache.GetObject(this.imageId);
if (currentImage == null) return; // Indicates image not available (e.g. not completely loaded yet)
// Turn off image smoothing
context.imageSmoothingEnabled = (<any>context).msImageSmoothingEnabled = this.smoothed;
// Compute destination rectangle
var centered = true;
var scaleX = canvas.clientWidth / currentImage.naturalWidth;
var scaleY = canvas.clientHeight / currentImage.naturalHeight;
switch (this.positionType) {
case "fill":
scaleX = scaleY = Math.max(scaleX, scaleY);
break;
case "fit":
scaleX = scaleY = Math.min(scaleX, scaleY);
break;
case "stretch":
// Do nothing
break;
case "manual":
scaleX = scaleY = this.scale;
centered = this.position == null;
break;
}
var width = currentImage.naturalWidth * scaleX;
var height = currentImage.naturalHeight * scaleY;
var x = centered ? (canvas.clientWidth - width) / 2.0 : this.position[0];
var y = centered ? (canvas.clientHeight - height) / 2.0 : this.position[1];
// Draw
var pixelRatio = window.devicePixelRatio;
context.drawImage(currentImage, 0, 0, currentImage.naturalWidth, currentImage.naturalHeight, x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio);
}
}
class VideoPrimitive extends Primitive {
readonly videoId: string;
readonly positionType: string;
readonly position: Float32Array
readonly scale: number;
readonly smoothed: boolean;
constructor(videoId: string, positionType: string, position: Float32Array, scale: number, smoothed: boolean, layerId: string) {
super(layerId);
this.videoId = videoId;
this.positionType = positionType;
this.position = position;
this.scale = scale;
this.smoothed = smoothed;
}
GetObjectCacheId() {
return this.videoId;
}
Draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, objectCache: ObjectCache) {
// Get current image
let currentVideo: HTMLVideoElement = objectCache.GetObject(this.videoId);
if (currentVideo == null) return; // Indicates video not available (e.g. not completely loaded yet)
// Turn off image smoothing
context.imageSmoothingEnabled = (<any>context).msImageSmoothingEnabled = this.smoothed;
// Compute destination rectangle
let centered = true;
let scaleX = canvas.clientWidth / currentVideo.videoWidth;
let scaleY = canvas.clientHeight / currentVideo.videoHeight;
switch (this.positionType) {
case "fill":
scaleX = scaleY = Math.max(scaleX, scaleY);
break;
case "fit":
scaleX = scaleY = Math.min(scaleX, scaleY);
break;
case "stretch":
// Do nothing
break;
case "manual":
scaleX = scaleY = this.scale;
centered = this.position == null;
break;
}
let width = currentVideo.videoWidth * scaleX;
let height = currentVideo.videoHeight * scaleY;
let x = centered ? (canvas.clientWidth - width) / 2.0 : this.position[0];
let y = centered ? (canvas.clientHeight - height) / 2.0 : this.position[1];
// Draw
let pixelRatio = window.devicePixelRatio;
context.drawImage(currentVideo, 0, 0, currentVideo.videoWidth, currentVideo.videoHeight, x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio);
}
}
export default class Canvas2D extends CanvasBase {
context: CanvasRenderingContext2D = null; // Rendering context
backgroundStyle: string; // Background color
// Dictionary from primitiveId to layer settings (wireframe, filled, opacity)
layerSettings: { [layerId: string]: { [key: string]: number | boolean } } = {};
layerIds: string[] = [];
globalFill = true;
globalOpacity = 1.0;
framePrimitives: Primitive[][] = []; // The primitives for each frame [frameIndex]
currentPrimitives: Primitive[] = null; // The primitives for the currently displayed frame
frameCoordinates: Float32Array[] = []; // The coordinates for each frame [frameIndex]
center: vec2;
scale: number;
angle: number;
focusPoint: vec2;
focusPointDelta: vec2;
transform: mat3;
constructor(canvasId: string, public frameRate: number, public width: number, public height: number, objectCache: ObjectCache, public SetStatus: (status: string) => void, public SetWarning: (message: string) => void, public RequestRedraw: () => void, public ReportFrameIdChange: (canvasId: string, frameId: string) => void) {
// Base class constructor
super(canvasId, frameRate, width, height, objectCache, SetStatus, SetWarning, RequestRedraw, ReportFrameIdChange);
// Setup 2D context for drawing
this.context = this.htmlCanvas.getContext('2d');
this.backgroundStyle = "#000000";
this.center = vec2.fromValues(width, height);
vec2.scale(this.center, this.center, window.devicePixelRatio / 2);
this.ResetView();
// Start render loop
this.StartRenderLoop();
}
// Adds UI for the control of certain layers
SetLayerSettings(layerSettings: any) {
// Delete any previous controls
while (this.dropdownTable.childElementCount > 2)
this.dropdownTable.removeChild(this.dropdownTable.lastChild);
this.layerSettings = layerSettings;
this.layerIds = [null];
for (let layerId in layerSettings) {
this.layerIds.push(layerId);
}
// Add header row
var BorderStyle = "1px solid #cccccc";
var headerRow = document.createElement("tr");
headerRow.style.borderBottom = BorderStyle;
for (var headerName of ["Visible", "Opacity", "Layer Id"]) {
var headerItem = document.createElement("th");
headerItem.className = "scenepic-dropdown-header";
headerItem.innerHTML = headerName;
headerRow.appendChild(headerItem);
}
this.dropdownTable.appendChild(headerRow);
// Create row helper function
var createRow = (id, label) => {
// Create fill checkbox
var checkboxFill = document.createElement("input");
checkboxFill.type = "checkbox";
checkboxFill.checked = this.ShowLayerFilled(id);
checkboxFill.addEventListener("change", event => { this.SetLayerFilled(id, checkboxFill.checked); this.PrepareBuffers(); event.stopPropagation(); });
// Create opacity slider
var sliderOpacity = document.createElement("input");
sliderOpacity.type = "range";
sliderOpacity.min = "0";
sliderOpacity.max = "100";
sliderOpacity.value = String(100.0 * this.GetLayerOpacity(id));
sliderOpacity.addEventListener("change", event => { this.SetLayerOpacity(id, Number(sliderOpacity.value) / 100.0); this.PrepareBuffers(); event.stopPropagation(); })
sliderOpacity.style.width = "50px";
sliderOpacity.style.height = "10px";
// Add layer label
var labelLayer = document.createElement("label");
labelLayer.appendChild(document.createTextNode(label));
return [checkboxFill, sliderOpacity, labelLayer];
};
var addRow = (rowItems, border) => {
// Create table row
var tr = document.createElement("tr");
if (border)
tr.style.borderTop = BorderStyle;
var addControl = (el, className) => {
var td = document.createElement("td");
td.appendChild(el);
td.className = className;
tr.appendChild(td);
};
addControl(rowItems[0], "scenepic-table-control");
addControl(rowItems[1], "scenepic-table-control");
addControl(rowItems[2], "scenepic-table-layerid");
this.dropdownTable.appendChild(tr);
};
// Add new controls
Object.keys(layerSettings).forEach(id => addRow(createRow(id, id), false));
addRow(createRow("<<<GLOBAL>>>", "Global"), true);
}
ShowLayerFilled(layerId: string): boolean {
if (layerId == "<<<GLOBAL>>>")
return this.globalFill;
if (layerId == null)
return true;
if (!(layerId in this.layerSettings))
return true;
if ("filled" in this.layerSettings[layerId] && !this.layerSettings[layerId]["filled"])
return false;
return true;
}
ToggleLayerFilled(index: number) {
if (index >= this.layerIds.length) {
return
}
let layerId = this.layerIds[index];
let filled = this.ShowLayerFilled(layerId);
this.SetLayerFilled(layerId, !filled);
}
SetLayerFilled(layerId: string, filled: boolean) {
if (layerId == null)
return;
if (layerId == "<<<GLOBAL>>>") {
this.globalFill = filled;
}
else {
if (!(layerId in this.layerSettings))
this.layerSettings[layerId] = {};
this.layerSettings[layerId]["filled"] = filled;
}
// Prepare buffers and force redraw
this.PrepareBuffers();
}
GetLayerOpacity(layerId: string): number {
if (layerId == "<<<GLOBAL>>>")
return this.globalOpacity;
if (layerId == null || !(layerId in this.layerSettings) || !("opacity" in this.layerSettings[layerId]))
return 1.0; // Opaque
else
return <number>this.layerSettings[layerId]["opacity"];
}
SetLayerOpacity(layerId: string, opacity: number) {
if (layerId == null)
return;
if (layerId == "<<<GLOBAL>>>") {
this.globalOpacity = opacity;
}
else {
if (!(layerId in this.layerSettings))
this.layerSettings[layerId] = {};
this.layerSettings[layerId]["opacity"] = opacity;
}
// Prepare buffers and force redraw
this.PrepareBuffers();
}
GetLayerRenderOrder(layerId: string): number {
if (layerId == null || !(layerId in this.layerSettings) || !("renderOrder" in this.layerSettings[layerId]))
return -1e3; // Backmost layer - bit of a hack
else
return <number>this.layerSettings[layerId]["renderOrder"];
}
// Execute a single canvas command
ExecuteCanvasCommand(command: any) {
switch (command["CommandType"]) {
case "SetBackgroundStyle":
this.backgroundStyle = String(command["Value"]);
break;
case "SetLayerSettings":
var layerSettings = Misc.GetDefault(command, "Value", null);
if (layerSettings != null)
this.SetLayerSettings(layerSettings);
break;
case "SetLayers":
this.layerIds = command["LayerIds"];
break;
default:
super.ExecuteCanvasCommand(command);
break;
}
}
AddPrimitive(frameIndex: number, primitive: Primitive) {
if (primitive.layerId == null) {
this.framePrimitives[frameIndex].push(primitive);
} else {
if (!(primitive.layerId in this.layerSettings)) {
this.layerSettings[primitive.layerId] = {};
this.SetLayerSettings(this.layerSettings);
}
let renderOrder = this.GetLayerRenderOrder(primitive.layerId);
let index = 0;
let lastId: string = null;
for (let other of this.framePrimitives[frameIndex]) {
if (other.layerId == lastId) {
index += 1;
continue;
}
if (other.layerId == primitive.layerId) {
break;
}
lastId = other.layerId;
let otherOrder = this.GetLayerRenderOrder(other.layerId);
if (renderOrder < otherOrder) {
break;
}
index += 1;
}
this.framePrimitives[frameIndex].splice(index, 0, primitive);
}
if (this.currentFrameIndex == frameIndex)
this.PrepareBuffers();
}
SetCoordinates(frameIndex: number, coordinates: Float32Array) {
this.frameCoordinates[frameIndex] = coordinates;
}
GetCoordinate(frameIndex: number, index: number) {
let start = index * 2;
let end = start + 2;
return this.frameCoordinates[frameIndex].slice(start, end);
}
AddLines(frameIndex: number, lines: Uint16Array, style: Uint8Array, width: Float32Array, layerIds: [string, number][]) {
let numLines = width.length;
let layerId = null;
let nextLayer = numLines;
let currentLayer = 0;
if (layerIds != null) {
layerId = layerIds[0][0];
nextLayer = currentLayer + layerIds[0][1];
}
for (let i = 0; i < numLines; ++i) {
let info = lines.slice(i * 3, i * 3 + 3);
let coordinates = this.frameCoordinates[frameIndex].slice(info[0] * 2, info[1] * 2);
let closePath = info[2] == 1;
let strokeStyle = Misc.StyleToHtmlHex(style.slice(i * 8, i * 8 + 4));
let fillStyle = Misc.StyleToHtmlHex(style.slice(i * 8 + 4, i * 8 + 8));
let lineWidth = width[i];
if (i == nextLayer) {
currentLayer += 1;
layerId = layerIds[currentLayer][0];
nextLayer = i + layerIds[currentLayer][1]
}
this.AddPrimitive(frameIndex, new LinesPrimitive(strokeStyle, lineWidth, fillStyle, coordinates, closePath, layerId))
}
}
AddCircles(frameIndex: number, circles: Float32Array, style: Uint8Array, layerIds: [string, number][]) {
let numCircles = style.length / 8;
let layerId = null;
let nextLayer = numCircles;
let currentLayer = 0;
if (layerIds != null) {
layerId = layerIds[0][0];
nextLayer = currentLayer + layerIds[0][1];
}
for (let i = 0; i < numCircles; ++i) {
let info = circles.slice(i * 4, i * 4 + 4);
let center = info.slice(0, 2);
let radius = info[2];
let lineWidth = info[3];
let strokeStyle = Misc.StyleToHtmlHex(style.slice(i * 8, i * 8 + 4));
let fillStyle = Misc.StyleToHtmlHex(style.slice(i * 8 + 4, i * 8 + 8));
if (i == nextLayer) {
currentLayer += 1;
layerId = layerIds[currentLayer][0];
nextLayer = i + layerIds[currentLayer][1]
}
this.AddPrimitive(frameIndex, new CirclePrimitive(strokeStyle, lineWidth, fillStyle, center, radius, layerId))
}
}
// Execute a single frame command
ExecuteFrameCommand(command: any, frameIndex: number) {
switch (command["CommandType"]) {
case "SetCoordinates":
var coordinates = Misc.Base64ToFloat32Array(command["CoordinateBuffer"])
this.SetCoordinates(frameIndex, coordinates);
break;
case "AddImage":
var imageId = command["ImageId"];
var index = command["Index"];
var positionType = Misc.GetDefault(command, "PositionType", "fit");
var position = this.GetCoordinate(frameIndex, index);
var scale = Misc.GetDefault(command, "Scale", 1.0);
var smoothed = Misc.GetDefault(command, "Smoothed", false);
var layerId = Misc.GetDefault(command, "LayerId", null);
this.AddPrimitive(frameIndex, new ImagePrimitive(imageId, positionType, position, scale, smoothed, layerId));
break;
case "AddVideo":
var index = command["Index"];
var positionType = Misc.GetDefault(command, "PositionType", "fit");
var position = this.GetCoordinate(frameIndex, index);
var scale = Misc.GetDefault(command, "Scale", 1.0);
var smoothed = Misc.GetDefault(command, "Smoothed", false);
var layerId = Misc.GetDefault(command, "LayerId", null);
this.AddPrimitive(frameIndex, new VideoPrimitive(this.mediaId, positionType, position, scale, smoothed, layerId));
break;
case "AddLines":
var lines = Misc.Base64ToUInt16Array(command["InfoBuffer"]);
var style = Misc.Base64ToUInt8Array(command["StyleBuffer"]);
var width = Misc.Base64ToFloat32Array(command["WidthBuffer"]);
var layerIds = Misc.GetDefault(command, "LayerIds", null);
this.AddLines(frameIndex, lines, style, width, layerIds);
break;
case "AddCircles":
var circles = Misc.Base64ToFloat32Array(command["InfoBuffer"]);
var style = Misc.Base64ToUInt8Array(command["StyleBuffer"]);
var layerIds = Misc.GetDefault(command, "LayerIds", null);
this.AddCircles(frameIndex, circles, style, layerIds);
break;
case "AddText":
var text = command["Text"];
var fillStyle = command["FillStyle"];
var sizeInPixels = command["SizeInPixels"]
var fontFamily = command["Font"];
var index = command["Index"];
var position = this.GetCoordinate(frameIndex, index);
var layerId = Misc.GetDefault(command, "LayerId", null);
this.AddPrimitive(frameIndex, new TextPrimitive(text, fillStyle, sizeInPixels, fontFamily, position, layerId));
break;
default:
super.ExecuteFrameCommand(command, frameIndex);
break;
}
}
AllocateFrame() {
this.framePrimitives.push([]);
this.frameCoordinates.push(null);
}
DeallocateFrame(frameIndex: number) {
this.framePrimitives[frameIndex] = [];
this.frameCoordinates[frameIndex] = null;
}
NotifyTextureUpdated(textureId: string) {
}
PrepareBuffers(): void {
super.PrepareBuffers();
var currentFrameIndex = this.currentFrameIndex;
// Get new primitives
var newPrimitives = this.framePrimitives[currentFrameIndex].slice();
// Deal with image caching (only applies if primitives are actually ImagePrimitives or VideoPrimitives)
for (var primitive of newPrimitives) {
var cacheId = primitive.GetObjectCacheId();
if (cacheId != null) this.objectCache.AddUser(cacheId);
}
if (this.currentPrimitives != null) {
for (var primitive of this.currentPrimitives) {
var cacheId = primitive.GetObjectCacheId();
if (cacheId != null) this.objectCache.RemoveUser(cacheId);
}
}
// Store current primitive
this.currentPrimitives = newPrimitives;
}
ResetView(): void {
this.focusPointDelta = vec2.create();
this.focusPoint = vec2.copy(vec2.create(), this.center);
this.scale = 1;
this.angle = 0;
this.updateTransform();
this.setTransform();
}
ComputeCameraTwist(point: vec2, event: PointerEvent) {
const old = this.pointerCoords.get(event.pointerId);
if (old == undefined) {
return 0;
}
const dx = point[0] - old[0];
const dy = old[1] - point[1];
let delta = dx;
if (Math.abs(dy) > Math.abs(dx)) {
delta = dy;
}
return 2 * delta / (window.devicePixelRatio * this.htmlCanvas.clientWidth);
}
private updateTransform() {
const toFocus = mat3.fromTranslation(mat3.create(), this.focusPoint);
const fromCenter = mat3.fromTranslation(mat3.create(), vec2.negate(vec2.create(), this.center));
const scale = mat3.fromScaling(mat3.create(), vec2.fromValues(this.scale, this.scale));
const rotate = mat3.fromRotation(mat3.create(), this.angle);
this.transform = mat3.create();
mat3.multiply(this.transform, fromCenter, this.transform);
mat3.multiply(this.transform, scale, this.transform);
mat3.multiply(this.transform, rotate, this.transform);
mat3.multiply(this.transform, toFocus, this.transform);
}
private setTransform(): void {
this.context.setTransform(
this.transform[0], this.transform[1],
this.transform[3], this.transform[4],
this.transform[6], this.transform[7])
}
// Render scene method
Render() {
// Clear
this.context.fillStyle = this.backgroundStyle;
this.context.resetTransform();
let width = this.htmlCanvas.clientWidth * window.devicePixelRatio;
let height = this.htmlCanvas.clientHeight * window.devicePixelRatio;
this.context.fillRect(0, 0, width, height);
this.setTransform();
// Composite primitives
if (this.currentPrimitives == null) return;
for (var primitive of this.currentPrimitives) {
let opacity = this.GetLayerOpacity(primitive.layerId) * this.globalOpacity;
let filled = this.ShowLayerFilled(primitive.layerId) && this.globalFill;
if (!filled || opacity == 0.0) {
continue;
}
this.context.globalAlpha = opacity;
primitive.Draw(this.htmlCanvas, this.context, this.objectCache);
this.context.globalAlpha = 1.0;
}
}
StartPlaying() {
// pre-cache
for (let primitives of this.framePrimitives) {
for (let primitive of primitives) {
let cacheId = primitive.GetObjectCacheId();
if (cacheId != null) {
this.objectCache.AddUser(cacheId);
}
}
}
super.StartPlaying();
}
StopPlaying() {
super.StopPlaying();
// free up the cache
for (let primitives of this.framePrimitives) {
for (let primitive of primitives) {
let imageId = primitive.GetObjectCacheId();
if (imageId != null) {
this.objectCache.RemoveUser(imageId);
}
}
}
}
HandleKeyDown(key: string): [boolean, boolean] {
var result = super.HandleKeyDown(key);
if (result[0]) return result; // Already handled
let handled = true;
key = key.toLowerCase();
switch (key) {
case "r":
this.ResetView();
break;
default:
handled = false;
break;
}
return [handled, false];
}
pointerCenter(): vec2 {
if (this.pointerCoords.size == 1) {
return this.pointerCoords.get(this.pid0);
} else {
const p0 = this.pointerCoords.get(this.pid0);
const p1 = this.pointerCoords.get(this.pid1);
let center = vec2.add(vec2.create(), p0, p1);
vec2.scale(center, center, 0.5);
return center;
}
}
updateFocusPointDelta(): void {
if (this.pointerCoords.size == 0) {
return
}
const center = this.pointerCenter();
const focusPoint = vec2.scale(vec2.create(), this.focusPoint, 1 / window.devicePixelRatio);
this.focusPointDelta = vec2.subtract(vec2.create(), focusPoint, center);
}
HandlePointerDown(point: vec2, event: PointerEvent): void {
super.HandlePointerDown(point, event);
this.updateFocusPointDelta();
this.updateTransform();
}
HandlePointerUp(event: PointerEvent): void {
super.HandlePointerUp(event);
this.updateFocusPointDelta();
this.updateTransform();
}
HandlePointerMoveWithTwist(point: vec2, twistAngle: number, event: PointerEvent): void {
const countPointers = this.pointerCoords.size;
if (countPointers == 0 || countPointers > 2) return;
if (countPointers == 2) {
const pinchZoom = this.PinchZoom(point, event);
this.scale = this.scale * pinchZoom.distanceRatio;
this.angle = this.angle + pinchZoom.angleDelta;
// Set focus to mean of two points
vec2.add(this.focusPoint, pinchZoom.center, this.focusPointDelta);
vec2.scale(this.focusPoint, this.focusPoint, window.devicePixelRatio);
} else {
this.angle += twistAngle;
if (twistAngle == 0) {
vec2.add(this.focusPoint, point, this.focusPointDelta);
vec2.scale(this.focusPoint, this.focusPoint, window.devicePixelRatio);
}
}
this.updateTransform();
this.setTransform();
super.HandlePointerMove(point, event);
}
HandleMouseWheel(event: WheelEvent): void {
const factor = Math.pow(1.1, event.deltaY > 0 ? -1 : 1);
this.scale *= factor;
this.updateTransform();
this.setTransform();
}
}