signal-desktop/ts/mediaEditor/MediaEditorFabricDigitalTimeSticker.ts

215 lines
5.3 KiB
TypeScript

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { fabric } from 'fabric';
import { customFabricObjectControls } from './util/customFabricObjectControls';
import { moreStyles } from './util/moreStyles';
import { getDateTimeFormatter } from '../util/formatTimestamp';
export enum DigitalClockStickerStyle {
White = 'White',
Black = 'Black',
Light = 'Light',
Dark = 'Dark',
Orange = 'Orange',
}
function getTextStyle(style: DigitalClockStickerStyle): {
fill: string;
textBackgroundColor: string;
} {
if (style === DigitalClockStickerStyle.Black) {
return {
fill: '#000',
textBackgroundColor: '',
};
}
if (style === DigitalClockStickerStyle.Light) {
return {
fill: '#fff',
textBackgroundColor: 'rgba(255, 255, 255, 0.4)',
};
}
if (style === DigitalClockStickerStyle.Dark) {
return {
fill: '#fff',
textBackgroundColor: 'rgba(0, 0, 0, 0.4)',
};
}
if (style === DigitalClockStickerStyle.Orange) {
return {
fill: '#ff7629',
textBackgroundColor: 'rgba(0, 0, 0, 0.6)',
};
}
return {
fill: '#fff',
textBackgroundColor: '',
};
}
const TEXT_PROPS = {
editable: false,
fontWeight: '400',
left: 0,
lockScalingFlip: true,
originX: 'center',
originY: 'center',
textAlign: 'center',
top: 0,
};
export class MediaEditorFabricDigitalTimeSticker extends fabric.Group {
static getNextStyle(
style?: DigitalClockStickerStyle
): DigitalClockStickerStyle {
if (style === DigitalClockStickerStyle.White) {
return DigitalClockStickerStyle.Black;
}
if (style === DigitalClockStickerStyle.Black) {
return DigitalClockStickerStyle.Light;
}
if (style === DigitalClockStickerStyle.Light) {
return DigitalClockStickerStyle.Dark;
}
if (style === DigitalClockStickerStyle.Dark) {
return DigitalClockStickerStyle.Orange;
}
return DigitalClockStickerStyle.White;
}
constructor(
timestamp: number,
style: DigitalClockStickerStyle = DigitalClockStickerStyle.White,
options: fabric.IGroupOptions = {}
) {
const parts = getDateTimeFormatter({
hour: 'numeric',
minute: 'numeric',
}).formatToParts(timestamp);
const { fill } = getTextStyle(style);
let dayPeriodText = '';
const timeText = parts.reduce((acc, part) => {
if (part.type === 'dayPeriod') {
dayPeriodText = part.value;
return acc;
}
return `${acc}${part.value}`;
}, '');
const timeTextNode = new fabric.IText(timeText.trim(), {
...TEXT_PROPS,
fill,
fontSize: 72,
fontFamily: '"Hatsuishi Large", Hatsuishi, Inter',
});
const dayPeriodTextNode = new fabric.IText(dayPeriodText, {
...TEXT_PROPS,
fill,
fontSize: 11,
fontFamily: 'Inter',
});
const dayPeriodBounds = dayPeriodTextNode.getBoundingRect();
const timeBounds = timeTextNode.getBoundingRect();
const totalWidth = dayPeriodBounds.width + timeBounds.width;
dayPeriodTextNode.set({
left: totalWidth / 2 + dayPeriodBounds.width / 2,
top: timeBounds.height / 2 - dayPeriodBounds.height * 1.66,
});
super([timeTextNode, dayPeriodTextNode], {
...options,
data: { stickerStyle: style, timestamp },
});
this.set('width', totalWidth * 2);
}
static override 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: (_: MediaEditorFabricDigitalTimeSticker) => unknown
): MediaEditorFabricDigitalTimeSticker {
const timestamp = options?.data.timestamp ?? Date.now();
const result = new MediaEditorFabricDigitalTimeSticker(
timestamp,
options.data?.stickerStyle,
options
);
callback(result);
return result;
}
override render(ctx: CanvasRenderingContext2D): void {
const { textBackgroundColor } = getTextStyle(this.data.stickerStyle);
if (textBackgroundColor) {
const bounds = this.getBoundingRect();
const zoom = this.canvas?.getZoom() || 1;
const height = bounds.height / zoom;
const left = bounds.left / zoom;
const top = bounds.top / zoom;
const width = bounds.width / zoom;
ctx.save();
ctx.fillStyle = textBackgroundColor;
ctx.beginPath();
ctx.roundRect(left, top, width, height, 14);
ctx.closePath();
ctx.fill();
ctx.restore();
}
super.render(ctx);
}
}
const moreStylesControl = new fabric.Control({
...moreStyles,
mouseUpHandler: (_eventData, { target }) => {
const stickerStyle = MediaEditorFabricDigitalTimeSticker.getNextStyle(
target.data.stickerStyle
);
target.setOptions({
data: {
...target.data,
stickerStyle,
},
});
const styleAttrs = getTextStyle(stickerStyle);
const group = target as fabric.Group;
group.getObjects().forEach(textObject => {
textObject.set({ fill: styleAttrs.fill });
});
target.setCoords();
target.canvas?.requestRenderAll();
return true;
},
});
MediaEditorFabricDigitalTimeSticker.prototype.type =
'MediaEditorFabricDigitalTimeSticker';
MediaEditorFabricDigitalTimeSticker.prototype.controls = {
...customFabricObjectControls,
mb: moreStylesControl,
};