зеркало из https://github.com/microsoft/scenepic.git
817 строки
30 KiB
TypeScript
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();
|
|
}
|
|
}
|