Adds time stickers to MediaEditor
|
@ -45,4 +45,5 @@
|
||||||
hasCustomTitleBar: () => false,
|
hasCustomTitleBar: () => false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
window.getPreferredSystemLocales = () => ['en'];
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ClassyProvider } from '../ts/components/PopperRootContext';
|
||||||
import { I18n } from '../sticker-creator/util/i18n';
|
import { I18n } from '../sticker-creator/util/i18n';
|
||||||
import { StorybookThemeContext } from './StorybookThemeContext';
|
import { StorybookThemeContext } from './StorybookThemeContext';
|
||||||
import { ThemeType } from '../ts/types/Util';
|
import { ThemeType } from '../ts/types/Util';
|
||||||
|
import { setupI18n } from '../ts/util/setupI18n';
|
||||||
|
|
||||||
export const globalTypes = {
|
export const globalTypes = {
|
||||||
mode: {
|
mode: {
|
||||||
|
@ -37,6 +38,8 @@ export const globalTypes = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.i18n = setupI18n('en', messages);
|
||||||
|
|
||||||
const withModeAndThemeProvider = (Story, context) => {
|
const withModeAndThemeProvider = (Story, context) => {
|
||||||
const theme =
|
const theme =
|
||||||
context.globals.theme === 'light' ? ThemeType.light : ThemeType.dark;
|
context.globals.theme === 'light' ? ThemeType.light : ThemeType.dark;
|
||||||
|
|
|
@ -2579,6 +2579,18 @@
|
||||||
"message": "Recently used stickers will appear here.",
|
"message": "Recently used stickers will appear here.",
|
||||||
"description": "Shown in the sticker picker when there are no recent stickers to show."
|
"description": "Shown in the sticker picker when there are no recent stickers to show."
|
||||||
},
|
},
|
||||||
|
"icu:stickers__StickerPicker__recent": {
|
||||||
|
"messageformat": "Recents",
|
||||||
|
"description": "Title for all of the recent stickers"
|
||||||
|
},
|
||||||
|
"icu:stickers__StickerPicker__featured": {
|
||||||
|
"messageformat": "Featured",
|
||||||
|
"description": "Title for featured stickers"
|
||||||
|
},
|
||||||
|
"icu:stickers__StickerPicker__analog-time": {
|
||||||
|
"messageformat": "Analog time",
|
||||||
|
"description": "aria-label for the analog time sticker"
|
||||||
|
},
|
||||||
"stickers--StickerPreview--Title": {
|
"stickers--StickerPreview--Title": {
|
||||||
"message": "Sticker Pack",
|
"message": "Sticker Pack",
|
||||||
"description": "The title that appears in the sticker pack preview modal."
|
"description": "The title that appears in the sticker pack preview modal."
|
||||||
|
@ -5619,6 +5631,10 @@
|
||||||
"message": "There was an error when saving your settings. Please try again.",
|
"message": "There was an error when saving your settings. Please try again.",
|
||||||
"description": "Shown if there is an error when saving your preferred reaction settings. Should be very rare to see this message."
|
"description": "Shown if there is an error when saving your preferred reaction settings. Should be very rare to see this message."
|
||||||
},
|
},
|
||||||
|
"icu:MediaEditor__clock-more-styles": {
|
||||||
|
"messageformat": "More styles",
|
||||||
|
"description": "Action button for switching up the clock styles"
|
||||||
|
},
|
||||||
"MediaEditor__control--draw": {
|
"MediaEditor__control--draw": {
|
||||||
"message": "Draw",
|
"message": "Draw",
|
||||||
"description": "Label for the draw button in the media editor"
|
"description": "Label for the draw button in the media editor"
|
||||||
|
|
BIN
fonts/stories/Hatsuishi-Regular.woff2
Normal file
1
images/analog-time/4-center.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" fill="none"><circle cx="21" cy="21" r="21" fill="#fff"/><circle cx="21" cy="21" r="21" fill="#fff"/><circle cx="21" cy="21" r="21" fill="#D1FFC1"/></svg>
|
After Width: | Height: | Size: 216 B |
1
images/analog-time/Arabic-hour.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="200" fill="none"><rect width="14" height="200" fill="#FFBE20" rx="7"/></svg>
|
After Width: | Height: | Size: 135 B |
1
images/analog-time/Arabic-minute.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="280" fill="none"><path fill="#FFBE20" d="M5 280c-2.761 0-5-1.99-5-4.444V4.444C0 1.99 2.239 0 5 0s5 1.99 5 4.444v271.112C10 278.01 7.761 280 5 280Z"/></svg>
|
After Width: | Height: | Size: 214 B |
1
images/analog-time/Arabic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none"><path fill="#000" d="M600 300c0 165.685-134.315 300-300 300S0 465.685 0 300 134.315 0 300 0s300 134.315 300 300Z" opacity=".8"/><path fill="red" d="M310.002 295.313c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10 10 4.477 10 10Z"/><path fill="#FFBE20" d="M320 300c0 11.046-8.954 20-20 20s-20-8.954-20-20 8.954-20 20-20 20 8.954 20 20Z"/><path fill="#fff" d="M283.959 83.409v-54.47h-5.486c-1.175 5.565-5.407 8.23-9.953 8.23h-1.332v6.034h2.116c2.586 0 6.426-.94 8.15-3.057V83.41h6.505ZM326.026 83.409v-5.957h-23.277c5.878-5.721 10.502-10.424 14.421-15.361 5.565-6.662 8.229-13.167 8.229-19.593 0-9.64-5.407-14.735-14.969-14.735-9.718 0-15.596 5.487-15.596 14.656 0 1.568.156 3.057.47 4.468l7.054.862a24.213 24.213 0 0 1-.549-5.173c0-5.8 3.057-9.091 8.308-9.091 5.172 0 8.072 3.213 8.072 9.326 0 5.565-2.038 10.894-6.818 16.85-4.938 5.957-10.738 11.756-17.086 18.183v5.565h31.741ZM432.311 117.635V62.723h-5.531c-1.185 5.61-5.452 8.296-10.034 8.296h-1.344v6.084h2.134c2.607 0 6.478-.949 8.217-3.082v43.614h6.558ZM528.691 206.367v-6.004h-23.466c5.925-5.768 10.587-10.509 14.537-15.486 5.61-6.716 8.297-13.274 8.297-19.753 0-9.718-5.452-14.854-15.091-14.854-9.798 0-15.724 5.531-15.724 14.775 0 1.58.158 3.081.475 4.504l7.11.869a24.43 24.43 0 0 1-.553-5.215c0-5.847 3.082-9.165 8.375-9.165 5.215 0 8.139 3.239 8.139 9.402 0 5.61-2.055 10.982-6.874 16.987-4.978 6.005-10.825 11.852-17.225 18.331v5.609h32ZM525.901 427.653h-7.506v-36.187h-7.743l-20.148 36.74v4.74h21.491v13.432h6.4v-13.432h7.506v-5.293Zm-13.906 0h-15.249c1.422-1.739 2.528-3.714 3.95-6.242l7.98-14.933c1.501-2.924 2.45-5.057 3.319-7.822h.158c-.158 2.923-.158 4.977-.158 8.612v20.385ZM416.189 531.196c10.35 0 16.355-7.347 16.355-19.515 0-11.931-4.978-18.647-13.827-18.647-5.215 0-9.086 2.292-11.298 6.637l1.501-18.646h21.254v-5.926h-26.706l-2.291 31.051 6.479.711c.948-5.056 4.187-8.217 9.007-8.217 5.847 0 8.849 4.741 8.849 13.432 0 8.77-3.239 13.432-9.165 13.432-5.294 0-8.296-4.109-8.928-10.114l-7.032.791c.711 9.086 6.558 15.011 15.802 15.011ZM299.388 525.508c-5.531 0-9.561 2.607-11.773 7.032.158-14.064 3.556-20.938 10.351-20.938 4.898 0 7.427 3.082 8.059 9.481l7.032-1.185c-.949-9.244-6.163-13.826-15.012-13.826-11.062 0-17.225 8.138-17.225 28.838 0 20.464 5.531 28.444 16.988 28.444 9.876 0 15.881-7.19 15.881-19.278 0-11.694-5.294-18.568-14.301-18.568Zm-1.58 32.236c-5.926 0-9.64-4.74-10.114-17.066 1.501-6.005 4.741-9.639 9.956-9.639 5.688 0 9.007 4.819 9.007 13.116 0 9.007-3.319 13.589-8.849 13.589ZM174.488 527.7v-4.346c.079-15.96 5.135-31.999 15.802-45.589v-4.978h-30.577v6.163h23.466c-10.192 14.617-15.723 29.866-15.881 46.458v2.292h7.19ZM96.071 417.677c6.088-2.134 8.935-6.088 8.935-12.571 0-9.172-6.01-14.153-15.576-14.153-9.409 0-15.655 5.218-15.655 14.232 0 7.274 3.162 10.99 8.934 13.52-6.72 2.135-10.2 6.721-10.2 14.074 0 9.725 6.326 15.497 17.158 15.497 10.358 0 16.841-5.377 16.841-15.813 0-8.144-3.637-12.097-10.437-14.786Zm-6.72-21.664c5.613 0 8.934 2.926 8.934 9.093 0 6.246-1.897 8.618-7.59 10.753l-2.056-.712c-5.218-1.818-8.064-4.032-8.064-10.12 0-6.088 3.32-9.014 8.776-9.014Zm.316 46.886c-6.483 0-10.279-3.795-10.279-10.278 0-6.721 2.61-9.884 8.777-12.018l2.767.949c5.693 1.976 8.697 4.664 8.697 11.148 0 7.195-3.637 10.199-9.962 10.199ZM71.09 211.055v-54.912h-5.53c-1.186 5.609-5.453 8.296-10.035 8.296h-1.343v6.084h2.133c2.607 0 6.479-.949 8.217-3.082v43.614h6.558ZM100.778 212.24c11.456 0 17.935-7.269 17.935-28.681s-6.479-28.601-17.935-28.601c-11.457 0-17.935 7.189-17.935 28.601s6.478 28.681 17.935 28.681Zm0-5.61c-7.19 0-10.982-5.372-10.982-23.071 0-17.698 3.792-22.992 10.982-22.992 7.19 0 10.982 5.294 10.982 22.992 0 17.699-3.792 23.071-10.982 23.071ZM159.346 119.947V65.034h-5.531c-1.185 5.61-5.452 8.296-10.034 8.296h-1.343v6.084h2.133c2.607 0 6.479-.948 8.217-3.081v43.614h6.558ZM186.506 119.947V65.034h-5.531c-1.185 5.61-5.452 8.296-10.035 8.296h-1.343v6.084h2.134c2.607 0 6.478-.948 8.217-3.081v43.614h6.558ZM544.269 327.821c10.034 0 16.039-5.61 16.039-15.96 0-7.822-3.555-12.958-10.113-14.064 5.61-1.422 9.007-6.558 9.007-13.274 0-9.244-5.531-13.985-14.933-13.985-10.429 0-15.881 6.479-15.96 17.067l6.795 1.027c.079-8.612 2.844-12.405 9.007-12.405 5.057 0 8.059 2.844 8.059 8.77 0 6.558-3.081 10.272-8.849 10.272h-2.528v5.688h2.765c6.321 0 9.718 3.793 9.718 10.983 0 6.874-3.16 10.192-9.007 10.192-5.926 0-9.481-4.108-9.56-12.326l-6.953.791c.158 11.377 6.163 17.224 16.513 17.224ZM50.575 270.538c-10.034 0-15.96 7.032-15.96 18.015 0 11.535 5.689 17.777 14.696 17.777 5.136 0 9.402-2.37 11.615-6.953 0 18.014-3.24 22.913-10.193 22.913-5.214 0-7.98-3.555-8.612-9.876l-6.953 1.343c1.107 9.165 6.4 14.064 15.407 14.064 10.825 0 16.671-6.716 16.671-29.313 0-21.965-5.925-27.97-16.67-27.97Zm.158 30.261c-5.688 0-9.165-4.503-9.165-12.404s3.319-12.326 9.007-12.326c6.637 0 9.56 3.635 10.193 14.143-1.107 6.716-4.978 10.587-10.035 10.587Z"/></svg>
|
After Width: | Height: | Size: 4.8 KiB |
1
images/analog-time/Baton-hour.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="210" fill="none"><path fill="#fff" fill-rule="evenodd" d="M5 0 0 210h40L35 0H5Zm15 176.001c8.837 0 16-7.164 16-16 0-8.837-7.163-16-16-16s-16 7.163-16 16c0 8.836 7.163 16 16 16Z" clip-rule="evenodd" opacity=".98"/></svg>
|
After Width: | Height: | Size: 278 B |
1
images/analog-time/Baton-minute.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="308" fill="none"><path fill="#fff" fill-rule="evenodd" d="M5.76 0 0 308h40L34.24 0H5.76ZM20 273.944c8.837 0 16-7.162 16-15.996 0-8.835-7.163-15.997-16-15.997s-16 7.162-16 15.997c0 8.834 7.163 15.996 16 15.996Z" clip-rule="evenodd" opacity=".98"/></svg>
|
After Width: | Height: | Size: 311 B |
1
images/analog-time/Baton.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none"><circle cx="300" cy="300" r="300" fill="#000" opacity=".5"/><path fill="#fff" d="M291 42.001h20v64h-20zM291 493.999h20v64h-20zM421.334 71.567l17.32 10-32 55.426-17.32-10zM195.336 463.01l17.32 10-32 55.426-17.32-10zM519.434 162.34l10 17.32-55.426 32-10-17.32zM127.99 388.34l10 17.32-55.426 32-10-17.32zM529.439 420.34l-10 17.32-55.426-32 10-17.32zM137.988 194.341l-10 17.32-55.426-32 10-17.32zM438.656 518.436l-17.32 10-32-55.426 17.32-10zM212.66 126.99l-17.32 10-32-55.426 17.32-10zM42.004 309.998v-20h64v20zM494.531 309.998v-20h64v20z"/></svg>
|
After Width: | Height: | Size: 621 B |
1
images/analog-time/Dive-hour.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="138" fill="none"><path fill="#fff" d="M27.648 18.84A2 2 0 0 1 28 19.975V138H0V19.974a2 2 0 0 1 .352-1.133l12-17.445a2 2 0 0 1 3.296 0l12 17.445Z"/></svg>
|
After Width: | Height: | Size: 212 B |
1
images/analog-time/Dive-minute.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="267" fill="none"><path fill="#fff" d="M27.665 19.52A2 2 0 0 1 28 20.63V267H0V20.63a2 2 0 0 1 .335-1.11l12-18.02a2 2 0 0 1 3.33 0l12 18.02Z"/></svg>
|
After Width: | Height: | Size: 206 B |
1
images/analog-time/Dive.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none"><circle cx="300" cy="300" r="300" fill="#000" opacity=".8"/><rect width="28" height="80" x="286" y="488" fill="#fff" rx="2"/><rect width="28" height="80" x="286" y="488" fill="#fff" rx="2"/><rect width="28" height="80" x="286" y="488" fill="#D1FFC1" rx="2"/><rect width="28" height="80" x="32" y="314" fill="#fff" rx="2" transform="rotate(-90 32 314)"/><rect width="28" height="80" x="32" y="314" fill="#fff" rx="2" transform="rotate(-90 32 314)"/><rect width="28" height="80" x="32" y="314" fill="#D1FFC1" rx="2" transform="rotate(-90 32 314)"/><rect width="28" height="80" x="488" y="314" fill="#fff" rx="2" transform="rotate(-90 488 314)"/><rect width="28" height="80" x="488" y="314" fill="#fff" rx="2" transform="rotate(-90 488 314)"/><rect width="28" height="80" x="488" y="314" fill="#D1FFC1" rx="2" transform="rotate(-90 488 314)"/><circle cx="424.049" cy="86.941" r="23.875" fill="#fff" transform="rotate(30 424.049 86.94)"/><circle cx="424.049" cy="86.941" r="23.875" fill="#fff" transform="rotate(30 424.049 86.94)"/><circle cx="424.049" cy="86.941" r="23.875" fill="#D1FFC1" transform="rotate(30 424.049 86.94)"/><circle cx="178.028" cy="513.062" r="23.875" fill="#fff" transform="rotate(30 178.028 513.062)"/><circle cx="178.028" cy="513.062" r="23.875" fill="#fff" transform="rotate(30 178.028 513.062)"/><circle cx="178.028" cy="513.062" r="23.875" fill="#D1FFC1" transform="rotate(30 178.028 513.062)"/><circle cx="514.099" cy="176.989" r="23.875" fill="#fff" transform="rotate(60 514.099 176.989)"/><circle cx="514.099" cy="176.989" r="23.875" fill="#fff" transform="rotate(60 514.099 176.989)"/><circle cx="514.099" cy="176.989" r="23.875" fill="#D1FFC1" transform="rotate(60 514.099 176.989)"/><circle cx="87.978" cy="423.011" r="23.875" fill="#fff" transform="rotate(60 87.978 423.011)"/><circle cx="87.978" cy="423.011" r="23.875" fill="#fff" transform="rotate(60 87.978 423.011)"/><circle cx="87.978" cy="423.011" r="23.875" fill="#D1FFC1" transform="rotate(60 87.978 423.011)"/><circle cx="514.096" cy="423.01" r="23.875" fill="#fff" transform="rotate(120 514.096 423.01)"/><circle cx="514.096" cy="423.01" r="23.875" fill="#fff" transform="rotate(120 514.096 423.01)"/><circle cx="514.096" cy="423.01" r="23.875" fill="#D1FFC1" transform="rotate(120 514.096 423.01)"/><circle cx="87.976" cy="176.989" r="23.875" fill="#fff" transform="rotate(120 87.976 176.989)"/><circle cx="87.976" cy="176.989" r="23.875" fill="#fff" transform="rotate(120 87.976 176.989)"/><circle cx="87.976" cy="176.989" r="23.875" fill="#D1FFC1" transform="rotate(120 87.976 176.989)"/><circle cx="424.049" cy="513.06" r="23.875" fill="#fff" transform="rotate(150 424.049 513.06)"/><circle cx="424.049" cy="513.06" r="23.875" fill="#fff" transform="rotate(150 424.049 513.06)"/><circle cx="424.049" cy="513.06" r="23.875" fill="#D1FFC1" transform="rotate(150 424.049 513.06)"/><circle cx="178.028" cy="86.939" r="23.875" fill="#fff" transform="rotate(150 178.028 86.939)"/><circle cx="178.028" cy="86.939" r="23.875" fill="#fff" transform="rotate(150 178.028 86.939)"/><circle cx="178.028" cy="86.939" r="23.875" fill="#D1FFC1" transform="rotate(150 178.028 86.939)"/><path fill="#fff" d="M301.829 115.866c-.703 1.588-2.955 1.588-3.658 0l-32.435-73.307a2 2 0 0 1 1.829-2.809h64.87a2 2 0 0 1 1.829 2.81l-32.435 73.306Z"/><path fill="#fff" d="M301.829 115.866c-.703 1.588-2.955 1.588-3.658 0l-32.435-73.307a2 2 0 0 1 1.829-2.809h64.87a2 2 0 0 1 1.829 2.81l-32.435 73.306Z"/><path fill="#D1FFC1" d="M301.829 115.866c-.703 1.588-2.955 1.588-3.658 0l-32.435-73.307a2 2 0 0 1 1.829-2.809h64.87a2 2 0 0 1 1.829 2.81l-32.435 73.306Z"/></svg>
|
After Width: | Height: | Size: 3.6 KiB |
1
images/analog-time/Explorer-hour.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="149" fill="none"><path fill="#fff" d="M10 149c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0s10 4.477 10 10v129c0 5.523-4.477 10-10 10Z"/></svg>
|
After Width: | Height: | Size: 204 B |
1
images/analog-time/Explorer-minute.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="229" fill="none"><path fill="#fff" d="M10 229c-5.523 0-10-4.477-10-10V10C0 4.477 4.477 0 10 0s10 4.477 10 10v209c0 5.523-4.477 10-10 10Z"/></svg>
|
After Width: | Height: | Size: 204 B |
1
images/analog-time/Explorer.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none"><circle cx="300" cy="300" r="300" fill="#fff" opacity=".3"/><rect width="20" height="40" x="519.434" y="162.34" fill="#fff" rx="10" transform="rotate(60 519.434 162.34)"/><rect width="20" height="40" x="107.209" y="400.339" fill="#fff" rx="10" transform="rotate(60 107.209 400.339)"/><rect width="20" height="40" x="421.344" y="71.566" fill="#fff" rx="10" transform="rotate(30 421.344 71.566)"/><rect width="20" height="40" x="183.344" y="483.794" fill="#fff" rx="10" transform="rotate(30 183.344 483.794)"/><rect width="20" height="40" x="163.34" y="81.565" fill="#fff" rx="10" transform="rotate(-30 163.34 81.565)"/><rect width="20" height="40" x="401.34" y="493.794" fill="#fff" rx="10" transform="rotate(-30 401.34 493.794)"/><rect width="20" height="40" x="72.566" y="179.66" fill="#fff" rx="10" transform="rotate(-60 72.566 179.66)"/><rect width="20" height="40" x="484.789" y="417.66" fill="#fff" rx="10" transform="rotate(-60 484.789 417.66)"/><circle cx="300.002" cy="300" r="10" fill="red"/><circle cx="300" cy="300" r="22" fill="#fff"/><path fill="#fff" d="M285.472 80.644v-54.47h-7.288c-1.489 5.486-5.486 8.151-10.424 8.151h-1.332v8.15h1.959c2.9 0 6.505-.861 8.229-2.82v40.989h8.856ZM328.605 80.644V73.12h-22.336c5.956-5.408 10.345-9.64 13.95-14.107 5.33-6.27 7.916-12.383 7.916-18.732 0-10.267-5.799-15.282-16.223-15.282s-16.772 5.72-16.772 15.126c0 1.489.157 2.9.47 4.31l9.248 1.019a28.518 28.518 0 0 1-.47-5.016c0-5.172 2.586-8.15 7.132-8.15 4.467 0 6.975 2.82 6.975 8.307 0 5.408-1.881 10.267-6.426 15.753-4.468 5.33-10.111 10.737-17.243 17.32v6.976h33.779ZM300.276 536.569c-5.452 0-9.244 2.291-11.456 6.241.158-12.878 3.16-18.409 9.086-18.409 4.266 0 6.479 2.765 7.111 8.454l9.086-1.422c-.948-9.402-6.4-14.064-16.039-14.064-11.615 0-18.252 8.138-18.252 28.76 0 20.306 6.005 28.523 18.094 28.523 10.429 0 16.829-7.427 16.829-19.674 0-11.93-5.61-18.409-14.459-18.409Zm-2.528 30.972c-5.294 0-8.533-4.425-8.928-16.039 1.501-4.978 4.424-7.901 8.849-7.901 4.977 0 7.901 4.108 7.901 11.693 0 8.217-2.924 12.247-7.822 12.247ZM553.663 329.743c10.746 0 17.146-5.846 17.146-16.197 0-7.585-3.556-12.562-10.351-13.668 5.847-1.423 9.402-6.558 9.402-13.116 0-9.402-5.925-14.301-16.118-14.301-10.982 0-17.066 6.321-17.145 17.224l9.086 1.264c.079-7.98 2.529-11.298 7.822-11.298 4.425 0 6.953 2.449 6.953 7.901 0 5.847-2.765 9.244-7.98 9.244h-2.449v6.953h2.607c5.768 0 8.77 3.239 8.77 9.718 0 6.005-2.765 9.007-7.743 9.007-5.135 0-8.217-3.713-8.217-11.298l-9.323 1.027c.158 11.615 6.558 17.54 17.54 17.54ZM45.991 272.461c-10.745 0-16.987 7.19-16.987 18.33 0 11.378 5.768 17.778 15.012 17.778 4.82 0 8.928-1.976 11.14-6.005-.158 15.881-3.002 20.069-8.928 20.069-4.503 0-7.032-3.24-7.585-8.85l-9.007 1.581c.948 9.244 6.637 14.379 16.276 14.379 11.694 0 17.857-6.715 17.857-29.154 0-22.044-6.163-28.128-17.778-28.128Zm.158 29.155c-4.977 0-8.059-4.109-8.059-11.062 0-7.269 2.923-11.14 7.98-11.14 5.926 0 8.533 3.318 9.007 13.748-1.264 5.451-4.661 8.454-8.928 8.454Z"/></svg>
|
After Width: | Height: | Size: 3 KiB |
|
@ -54,3 +54,8 @@
|
||||||
font-family: 'EB Garamond';
|
font-family: 'EB Garamond';
|
||||||
src: url('../fonts/stories/EBGaramond-Regular.ttf');
|
src: url('../fonts/stories/EBGaramond-Regular.ttf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Hatsuishi';
|
||||||
|
src: url('../fonts/stories/Hatsuishi-Regular.woff2');
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
|
|
||||||
@mixin font-family {
|
@mixin localized-fonts {
|
||||||
font-family: $inter;
|
|
||||||
/* Japanese */
|
/* Japanese */
|
||||||
&:lang(ja) {
|
&:lang(ja) {
|
||||||
font-family: 'SF Pro JP', 'Hiragino Kaku Gothic Pro', 'ヒラギノ角ゴ Pro W3',
|
font-family: 'SF Pro JP', 'Hiragino Kaku Gothic Pro', 'ヒラギノ角ゴ Pro W3',
|
||||||
|
@ -18,6 +17,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin font-family {
|
||||||
|
font-family: $inter;
|
||||||
|
@include localized-fonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin time-fonts {
|
||||||
|
font-family: Hatsuishi, $inter;
|
||||||
|
@include localized-fonts;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin font-title-1 {
|
@mixin font-title-1 {
|
||||||
@include font-family;
|
@include font-family;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -5433,6 +5433,10 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-sticker-picker__recents--title {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
.module-sticker-picker__header__button {
|
.module-sticker-picker__header__button {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
@ -5616,15 +5620,18 @@ button.module-image__border-overlay:focus {
|
||||||
.module-sticker-picker__body {
|
.module-sticker-picker__body {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 8px;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-auto-rows: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
width: 332px;
|
width: 332px;
|
||||||
height: 356px;
|
height: 356px;
|
||||||
padding: 8px 13px 16px 13px;
|
padding: 8px 13px 16px 13px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: grid;
|
|
||||||
grid-gap: 8px;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
grid-auto-rows: 68px;
|
|
||||||
|
|
||||||
&--under-text {
|
&--under-text {
|
||||||
height: 320px;
|
height: 320px;
|
||||||
|
@ -5721,6 +5728,45 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-sticker-picker__time--digital {
|
||||||
|
@include time-fonts;
|
||||||
|
color: $color-white;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sticker-picker__time--analog {
|
||||||
|
background: url(../images/analog-time/Arabic.svg) center no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
height: 64px;
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sticker-picker__time--analog__hour {
|
||||||
|
background: url(../images/analog-time/Arabic-hour.svg) center no-repeat;
|
||||||
|
height: 14px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -14px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sticker-picker__time--analog__minute {
|
||||||
|
background: url(../images/analog-time/Arabic-minute.svg) center no-repeat;
|
||||||
|
height: 22px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -1px;
|
||||||
|
margin-top: -22px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform-origin: 50% 100%;
|
||||||
|
width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
// Module: Sticker button (launches the sticker picker)
|
// Module: Sticker button (launches the sticker picker)
|
||||||
|
|
||||||
.sticker-button-wrapper {
|
.sticker-button-wrapper {
|
||||||
|
|
|
@ -27,9 +27,11 @@ import { useFabricHistory } from '../mediaEditor/useFabricHistory';
|
||||||
import { usePortal } from '../hooks/usePortal';
|
import { usePortal } from '../hooks/usePortal';
|
||||||
import { useUniqueId } from '../hooks/useUniqueId';
|
import { useUniqueId } from '../hooks/useUniqueId';
|
||||||
|
|
||||||
import { MediaEditorFabricPencilBrush } from '../mediaEditor/MediaEditorFabricPencilBrush';
|
import { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker';
|
||||||
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
|
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
|
||||||
|
import { MediaEditorFabricDigitalTimeSticker } from '../mediaEditor/MediaEditorFabricDigitalTimeSticker';
|
||||||
import { MediaEditorFabricIText } from '../mediaEditor/MediaEditorFabricIText';
|
import { MediaEditorFabricIText } from '../mediaEditor/MediaEditorFabricIText';
|
||||||
|
import { MediaEditorFabricPencilBrush } from '../mediaEditor/MediaEditorFabricPencilBrush';
|
||||||
import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticker';
|
import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticker';
|
||||||
import { fabricEffectListener } from '../mediaEditor/fabricEffectListener';
|
import { fabricEffectListener } from '../mediaEditor/fabricEffectListener';
|
||||||
import { getRGBA, getHSL } from '../mediaEditor/util/color';
|
import { getRGBA, getHSL } from '../mediaEditor/util/color';
|
||||||
|
@ -1062,6 +1064,53 @@ export function MediaEditor({
|
||||||
fabricCanvas.setActiveObject(sticker);
|
fabricCanvas.setActiveObject(sticker);
|
||||||
setEditMode(undefined);
|
setEditMode(undefined);
|
||||||
}}
|
}}
|
||||||
|
onPickTimeSticker={(style: 'analog' | 'digital') => {
|
||||||
|
if (!fabricCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'digital') {
|
||||||
|
const sticker = new MediaEditorFabricDigitalTimeSticker(
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
sticker.setPositionByOrigin(
|
||||||
|
new fabric.Point(
|
||||||
|
imageState.width / 2,
|
||||||
|
imageState.height / 2
|
||||||
|
),
|
||||||
|
'center',
|
||||||
|
'center'
|
||||||
|
);
|
||||||
|
sticker.setCoords();
|
||||||
|
|
||||||
|
fabricCanvas.add(sticker);
|
||||||
|
fabricCanvas.setActiveObject(sticker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'analog') {
|
||||||
|
const sticker = new MediaEditorFabricAnalogTimeSticker();
|
||||||
|
const STICKER_SIZE_RELATIVE_TO_CANVAS = 4;
|
||||||
|
const size =
|
||||||
|
Math.min(imageState.width, imageState.height) /
|
||||||
|
STICKER_SIZE_RELATIVE_TO_CANVAS;
|
||||||
|
|
||||||
|
sticker.scaleToHeight(size);
|
||||||
|
sticker.setPositionByOrigin(
|
||||||
|
new fabric.Point(
|
||||||
|
imageState.width / 2,
|
||||||
|
imageState.height / 2
|
||||||
|
),
|
||||||
|
'center',
|
||||||
|
'center'
|
||||||
|
);
|
||||||
|
sticker.setCoords();
|
||||||
|
|
||||||
|
fabricCanvas.add(sticker);
|
||||||
|
fabricCanvas.setActiveObject(sticker);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditMode(undefined);
|
||||||
|
}}
|
||||||
receivedPacks={[]}
|
receivedPacks={[]}
|
||||||
recentStickers={recentStickers}
|
recentStickers={recentStickers}
|
||||||
showPickerHint={false}
|
showPickerHint={false}
|
||||||
|
@ -1247,7 +1296,7 @@ function getNewImageStateFromCrop(
|
||||||
|
|
||||||
function cloneFabricCanvas(original: fabric.Canvas): Promise<fabric.Canvas> {
|
function cloneFabricCanvas(original: fabric.Canvas): Promise<fabric.Canvas> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
original.clone(resolve);
|
original.clone(resolve, ['data']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ export type OwnProps = {
|
||||||
stickerId: number,
|
stickerId: number,
|
||||||
url: string
|
url: string
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||||
readonly showIntroduction?: boolean;
|
readonly showIntroduction?: boolean;
|
||||||
readonly clearShowIntroduction: () => unknown;
|
readonly clearShowIntroduction: () => unknown;
|
||||||
readonly showPickerHint: boolean;
|
readonly showPickerHint: boolean;
|
||||||
|
@ -51,6 +52,7 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
||||||
clearInstalledStickerPack,
|
clearInstalledStickerPack,
|
||||||
onClickAddPack,
|
onClickAddPack,
|
||||||
onPickSticker,
|
onPickSticker,
|
||||||
|
onPickTimeSticker,
|
||||||
recentStickers,
|
recentStickers,
|
||||||
onOpenStateChanged,
|
onOpenStateChanged,
|
||||||
receivedPacks,
|
receivedPacks,
|
||||||
|
@ -111,6 +113,14 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
||||||
[setOpen, onPickSticker]
|
[setOpen, onPickSticker]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handlePickTimeSticker = React.useCallback(
|
||||||
|
(style: 'analog' | 'digital') => {
|
||||||
|
setOpen(false);
|
||||||
|
onPickTimeSticker?.(style);
|
||||||
|
},
|
||||||
|
[setOpen, onPickTimeSticker]
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
@ -355,6 +365,9 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
||||||
onClickAddPack ? handleClickAddPack : undefined
|
onClickAddPack ? handleClickAddPack : undefined
|
||||||
}
|
}
|
||||||
onPickSticker={handlePickSticker}
|
onPickSticker={handlePickSticker}
|
||||||
|
onPickTimeSticker={
|
||||||
|
onPickTimeSticker ? handlePickTimeSticker : undefined
|
||||||
|
}
|
||||||
recentStickers={recentStickers}
|
recentStickers={recentStickers}
|
||||||
showPickerHint={showPickerHint}
|
showPickerHint={showPickerHint}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import FocusTrap from 'focus-trap-react';
|
||||||
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
import type { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
import type { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { getAnalogTime } from '../../util/getAnalogTime';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly i18n: LocalizerType;
|
readonly i18n: LocalizerType;
|
||||||
|
@ -18,6 +19,7 @@ export type OwnProps = {
|
||||||
stickerId: number,
|
stickerId: number,
|
||||||
url: string
|
url: string
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||||
readonly packs: ReadonlyArray<StickerPackType>;
|
readonly packs: ReadonlyArray<StickerPackType>;
|
||||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||||
readonly showPickerHint?: boolean;
|
readonly showPickerHint?: boolean;
|
||||||
|
@ -71,6 +73,7 @@ export const StickerPicker = React.memo(
|
||||||
onClose,
|
onClose,
|
||||||
onClickAddPack,
|
onClickAddPack,
|
||||||
onPickSticker,
|
onPickSticker,
|
||||||
|
onPickTimeSticker,
|
||||||
showPickerHint,
|
showPickerHint,
|
||||||
style,
|
style,
|
||||||
}: Props,
|
}: Props,
|
||||||
|
@ -131,7 +134,10 @@ export const StickerPicker = React.memo(
|
||||||
// Focus popup on after initial render, restore focus on teardown
|
// Focus popup on after initial render, restore focus on teardown
|
||||||
const [focusRef] = useRestoreFocus();
|
const [focusRef] = useRestoreFocus();
|
||||||
|
|
||||||
const isEmpty = stickers.length === 0;
|
const hasPacks = packs.length > 0;
|
||||||
|
const isRecents = hasPacks && currentTab === 'recents';
|
||||||
|
const hasTimeStickers = isRecents && onPickTimeSticker;
|
||||||
|
const isEmpty = stickers.length === 0 && !hasTimeStickers;
|
||||||
const addPackRef = isEmpty ? focusRef : undefined;
|
const addPackRef = isEmpty ? focusRef : undefined;
|
||||||
const downloadError =
|
const downloadError =
|
||||||
selectedPack &&
|
selectedPack &&
|
||||||
|
@ -142,14 +148,13 @@ export const StickerPicker = React.memo(
|
||||||
? selectedPack.stickerCount - stickers.length
|
? selectedPack.stickerCount - stickers.length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const hasPacks = packs.length > 0;
|
|
||||||
const isRecents = hasPacks && currentTab === 'recents';
|
|
||||||
const showPendingText = pendingCount > 0;
|
const showPendingText = pendingCount > 0;
|
||||||
const showDownlaodErrorText = downloadError;
|
const showDownloadErrorText = downloadError;
|
||||||
const showEmptyText = !downloadError && isEmpty;
|
const showEmptyText = !downloadError && isEmpty;
|
||||||
const showText =
|
const showText =
|
||||||
showPendingText || showDownlaodErrorText || showEmptyText;
|
showPendingText || showDownloadErrorText || showEmptyText;
|
||||||
const showLongText = showPickerHint;
|
const showLongText = showPickerHint;
|
||||||
|
const analogTime = getAnalogTime();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
|
@ -303,46 +308,97 @@ export const StickerPicker = React.memo(
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!isEmpty ? (
|
{!isEmpty ? (
|
||||||
<div
|
<div className="module-sticker-picker__body__content">
|
||||||
className={classNames(
|
{isRecents && onPickTimeSticker && (
|
||||||
'module-sticker-picker__body__content',
|
<div className="module-sticker-picker__recents">
|
||||||
{
|
<strong className="module-sticker-picker__recents__title">
|
||||||
|
{i18n('icu:stickers__StickerPicker__featured')}
|
||||||
|
</strong>
|
||||||
|
<div className="module-sticker-picker__body__grid">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="module-sticker-picker__body__cell module-sticker-picker__time--digital"
|
||||||
|
onClick={() => onPickTimeSticker('digital')}
|
||||||
|
>
|
||||||
|
{new Intl.DateTimeFormat(
|
||||||
|
window.getPreferredSystemLocales(),
|
||||||
|
{
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.formatToParts(Date.now())
|
||||||
|
.filter(x => x.type !== 'dayPeriod')
|
||||||
|
.reduce((acc, { value }) => `${acc}${value}`, '')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
aria-label={i18n(
|
||||||
|
'icu:stickers__StickerPicker__analog-time'
|
||||||
|
)}
|
||||||
|
className="module-sticker-picker__body__cell module-sticker-picker__time--analog"
|
||||||
|
onClick={() => onPickTimeSticker('analog')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="module-sticker-picker__time--analog__hour"
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${analogTime.hour}deg)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="module-sticker-picker__time--analog__minute"
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${analogTime.minute}deg)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{stickers.length > 0 && (
|
||||||
|
<strong className="module-sticker-picker__recents__title">
|
||||||
|
{i18n('icu:stickers__StickerPicker__recent')}
|
||||||
|
</strong>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={classNames('module-sticker-picker__body__grid', {
|
||||||
'module-sticker-picker__body__content--under-text':
|
'module-sticker-picker__body__content--under-text':
|
||||||
showText,
|
showText,
|
||||||
'module-sticker-picker__body__content--under-long-text':
|
'module-sticker-picker__body__content--under-long-text':
|
||||||
showLongText,
|
showLongText,
|
||||||
}
|
})}
|
||||||
)}
|
>
|
||||||
>
|
{stickers.map(({ packId, id, url }, index: number) => {
|
||||||
{stickers.map(({ packId, id, url }, index: number) => {
|
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
ref={maybeFocusRef}
|
ref={maybeFocusRef}
|
||||||
key={`${packId}-${id}`}
|
key={`${packId}-${id}`}
|
||||||
className="module-sticker-picker__body__cell"
|
className="module-sticker-picker__body__cell"
|
||||||
onClick={() => onPickSticker(packId, id, url)}
|
onClick={() => onPickSticker(packId, id, url)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="module-sticker-picker__body__cell__image"
|
className="module-sticker-picker__body__cell__image"
|
||||||
src={url}
|
src={url}
|
||||||
alt={packTitle}
|
alt={packTitle}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Array(pendingCount)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={i}
|
||||||
|
className="module-sticker-picker__body__cell__placeholder"
|
||||||
|
role="presentation"
|
||||||
/>
|
/>
|
||||||
</button>
|
))}
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
{Array(pendingCount)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => (
|
|
||||||
<div
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
key={i}
|
|
||||||
className="module-sticker-picker__body__cell__placeholder"
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
289
ts/mediaEditor/MediaEditorFabricAnalogTimeSticker.ts
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
// 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,
|
||||||
|
};
|
214
ts/mediaEditor/MediaEditorFabricDigitalTimeSticker.ts
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
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 = new Intl.DateTimeFormat(window.getPreferredSystemLocales(), {
|
||||||
|
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,
|
||||||
|
};
|
|
@ -7,6 +7,8 @@ import { fabric } from 'fabric';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
import type { ImageStateType } from './ImageStateType';
|
import type { ImageStateType } from './ImageStateType';
|
||||||
|
import { MediaEditorFabricAnalogTimeSticker } from './MediaEditorFabricAnalogTimeSticker';
|
||||||
|
import { MediaEditorFabricDigitalTimeSticker } from './MediaEditorFabricDigitalTimeSticker';
|
||||||
import { MediaEditorFabricIText } from './MediaEditorFabricIText';
|
import { MediaEditorFabricIText } from './MediaEditorFabricIText';
|
||||||
import { MediaEditorFabricPath } from './MediaEditorFabricPath';
|
import { MediaEditorFabricPath } from './MediaEditorFabricPath';
|
||||||
import { MediaEditorFabricSticker } from './MediaEditorFabricSticker';
|
import { MediaEditorFabricSticker } from './MediaEditorFabricSticker';
|
||||||
|
@ -152,6 +154,8 @@ export function useFabricHistory({
|
||||||
// doesn't make it easy to deserialize into a custom class without polluting the
|
// doesn't make it easy to deserialize into a custom class without polluting the
|
||||||
// global namespace. See <http://fabricjs.com/fabric-intro-part-3#subclassing>.
|
// global namespace. See <http://fabricjs.com/fabric-intro-part-3#subclassing>.
|
||||||
Object.assign(fabric, {
|
Object.assign(fabric, {
|
||||||
|
MediaEditorFabricAnalogTimeSticker,
|
||||||
|
MediaEditorFabricDigitalTimeSticker,
|
||||||
MediaEditorFabricIText,
|
MediaEditorFabricIText,
|
||||||
MediaEditorFabricPath,
|
MediaEditorFabricPath,
|
||||||
MediaEditorFabricSticker,
|
MediaEditorFabricSticker,
|
||||||
|
@ -216,7 +220,7 @@ export function useFabricHistory({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCanvasState(fabricCanvas: fabric.Canvas): string {
|
function getCanvasState(fabricCanvas: fabric.Canvas): string {
|
||||||
return JSON.stringify(fabricCanvas.toDatalessJSON());
|
return JSON.stringify(fabricCanvas.toDatalessJSON(['data']));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIsTimeTraveling({
|
function getIsTimeTraveling({
|
||||||
|
|
46
ts/mediaEditor/util/moreStyles.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
function render(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
left: number,
|
||||||
|
top: number
|
||||||
|
): void {
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.font = '11px Inter';
|
||||||
|
const text = window.i18n('icu:MediaEditor__clock-more-styles');
|
||||||
|
const textMetrics = ctx.measureText(text);
|
||||||
|
|
||||||
|
const boxHeight = textMetrics.fontBoundingBoxAscent * 2;
|
||||||
|
const boxWidth = textMetrics.width * 1.5;
|
||||||
|
const boxX = left - boxWidth / 2;
|
||||||
|
const textX = left - textMetrics.width / 2;
|
||||||
|
const textY = top + boxHeight / 1.5;
|
||||||
|
|
||||||
|
// box
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.strokeStyle = '#000000';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(boxX, top, boxWidth, boxHeight, 4);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// text
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillText(text, textX, textY);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moreStyles = {
|
||||||
|
cursorStyleHandler: (): 'pointer' => 'pointer',
|
||||||
|
offsetY: 20,
|
||||||
|
render,
|
||||||
|
sizeX: 100,
|
||||||
|
sizeY: 33,
|
||||||
|
withConnection: true,
|
||||||
|
x: 0,
|
||||||
|
y: 0.5,
|
||||||
|
};
|
16
ts/util/getAnalogTime.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
const HOURS = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330];
|
||||||
|
const NEXT_HOUR_DEG = 30;
|
||||||
|
|
||||||
|
export function getAnalogTime(): { hour: number; minute: number } {
|
||||||
|
const date = new Date();
|
||||||
|
const minutesBy60 = 60 / date.getMinutes();
|
||||||
|
const minute = 360 / minutesBy60;
|
||||||
|
const hourIndex = date.getHours() % 12;
|
||||||
|
const currentHour = HOURS[hourIndex] ?? 0;
|
||||||
|
const hour = currentHour + NEXT_HOUR_DEG / minutesBy60;
|
||||||
|
|
||||||
|
return { hour, minute };
|
||||||
|
}
|