signal-desktop/ts/mediaEditor/MediaEditorFabricAnalogTimeSticker.ts
2023-03-01 14:00:50 -05:00

289 lines
6.7 KiB
TypeScript

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { get } from 'lodash';
import { fabric } from 'fabric';
import { customFabricObjectControls } from './util/customFabricObjectControls';
import { getAnalogTime } from '../util/getAnalogTime';
import { strictAssert } from '../util/assert';
import { moreStyles } from './util/moreStyles';
export enum AnalogClockStickerStyle {
Arabic = 'Arabic',
Baton = 'Baton',
Explorer = 'Explorer',
Dive = 'Dive',
}
const HOUR_LENGTH = 0.44;
const MIN_LENGTH = 0.69;
type ClockAsset = {
dial: HTMLImageElement;
hour: HTMLImageElement;
minute: HTMLImageElement;
};
const ASSETS = new Map<AnalogClockStickerStyle, ClockAsset>();
function hydrateAssets(): void {
if (ASSETS.size) {
return;
}
const path = 'images/analog-time';
const clocks = [
AnalogClockStickerStyle.Arabic,
AnalogClockStickerStyle.Baton,
AnalogClockStickerStyle.Explorer,
AnalogClockStickerStyle.Dive,
];
clocks.forEach(name => {
const dial = new Image();
const hour = new Image();
const minute = new Image();
dial.src = `${path}/${name}.svg`;
hour.src = `${path}/${name}-hour.svg`;
minute.src = `${path}/${name}-minute.svg`;
ASSETS.set(name, {
dial,
hour,
minute,
});
});
}
function degToRad(deg: number): number {
return deg * (Math.PI / 180);
}
type HandDimensions = {
rad: number;
length: number;
width: number;
};
function drawHands(
ctx: CanvasRenderingContext2D,
clock: ClockAsset,
hourDimensions: HandDimensions,
minuteDimensions: HandDimensions,
offset = 0
): void {
ctx.rotate(hourDimensions.rad);
ctx.drawImage(
clock.hour,
0 - hourDimensions.width / 2,
0 - hourDimensions.length + offset,
hourDimensions.width,
hourDimensions.length
);
ctx.rotate(-hourDimensions.rad);
ctx.rotate(minuteDimensions.rad);
ctx.drawImage(
clock.minute,
0 - minuteDimensions.width / 2,
0 - minuteDimensions.length + offset,
minuteDimensions.width,
minuteDimensions.length
);
ctx.rotate(-minuteDimensions.rad);
}
export class MediaEditorFabricAnalogTimeSticker extends fabric.Image {
static getNextStyle(
style?: AnalogClockStickerStyle
): AnalogClockStickerStyle {
if (style === AnalogClockStickerStyle.Dive) {
return AnalogClockStickerStyle.Arabic;
}
if (style === AnalogClockStickerStyle.Explorer) {
return AnalogClockStickerStyle.Dive;
}
if (style === AnalogClockStickerStyle.Baton) {
return AnalogClockStickerStyle.Explorer;
}
return AnalogClockStickerStyle.Baton;
}
constructor(options: fabric.IImageOptions = {}) {
if (!ASSETS.size) {
hydrateAssets();
}
let style: AnalogClockStickerStyle = AnalogClockStickerStyle.Arabic;
ASSETS.forEach((asset, styleName) => {
if (get(options, 'src') === asset.dial.src) {
style = styleName;
}
});
const clock = ASSETS.get(style);
strictAssert(clock, 'expected clock not found');
super(clock.dial, {
...options,
data: { stickerStyle: style, timeDeg: getAnalogTime() },
});
this.on('modified', () => this.canvas?.bringToFront(this));
}
override render(ctx: CanvasRenderingContext2D): void {
super.render(ctx);
const { stickerStyle, timeDeg } = this.data;
const { x, y } = this.getCenterPoint();
const radius = this.getScaledHeight() / 2;
const flip = this.flipX || this.flipY ? 180 : 0;
const rawAngle = (this.angle ?? 0) - flip;
const timeRad = {
hour: degToRad(timeDeg.hour + rawAngle),
minute: degToRad(timeDeg.minute + rawAngle),
};
ctx.save();
ctx.translate(x, y);
const clock = ASSETS.get(stickerStyle);
strictAssert(clock, 'expected clock not found');
if (stickerStyle === AnalogClockStickerStyle.Arabic) {
const offset = radius * 0.106;
drawHands(
ctx,
clock,
{
rad: timeRad.hour,
length: radius * HOUR_LENGTH,
width: radius * 0.049,
},
{
rad: timeRad.minute,
length: radius * MIN_LENGTH,
width: radius * 0.036,
},
offset
);
}
if (stickerStyle === AnalogClockStickerStyle.Baton) {
const offset = radius * 0.106;
drawHands(
ctx,
clock,
{
rad: timeRad.hour,
length: radius * HOUR_LENGTH,
width: radius * 0.09,
},
{
rad: timeRad.minute,
length: radius * MIN_LENGTH,
width: radius * 0.09,
},
offset
);
}
if (stickerStyle === AnalogClockStickerStyle.Explorer) {
drawHands(
ctx,
clock,
{
rad: timeRad.hour,
length: radius * HOUR_LENGTH,
width: radius * 0.07,
},
{
rad: timeRad.minute,
length: radius * MIN_LENGTH,
width: radius * 0.07,
}
);
}
if (stickerStyle === AnalogClockStickerStyle.Dive) {
drawHands(
ctx,
clock,
{
rad: timeRad.hour,
length: radius * 0.47,
width: radius * 0.095,
},
{
rad: timeRad.minute,
length: radius * 0.89,
width: radius * 0.095,
}
);
// Circle
const circleSize = radius * 0.08;
ctx.fillStyle = '#d1ffc1';
ctx.strokeStyle = '#d1ffc1';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, circleSize, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
static fromObject(
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
options: any,
callback: (_: MediaEditorFabricAnalogTimeSticker) => unknown
): void {
callback(new MediaEditorFabricAnalogTimeSticker(options));
}
}
const moreStylesControl = new fabric.Control({
...moreStyles,
mouseUpHandler: (_eventData, { target }) => {
const stickerStyle = MediaEditorFabricAnalogTimeSticker.getNextStyle(
target.data.stickerStyle
);
target.setOptions({
data: {
...target.data,
stickerStyle,
},
});
const clock = ASSETS.get(stickerStyle);
strictAssert(clock, 'expected clock not found');
const img = target as fabric.Image;
img.setElement(clock.dial);
target.setCoords();
target.canvas?.requestRenderAll();
return true;
},
});
MediaEditorFabricAnalogTimeSticker.prototype.type =
'MediaEditorFabricAnalogTimeSticker';
MediaEditorFabricAnalogTimeSticker.prototype.borderColor = '#ffffff';
MediaEditorFabricAnalogTimeSticker.prototype.controls = {
...customFabricObjectControls,
mb: moreStylesControl,
};