Adds time stickers to MediaEditor
|
@ -45,4 +45,5 @@
|
|||
hasCustomTitleBar: () => false,
|
||||
},
|
||||
};
|
||||
window.getPreferredSystemLocales = () => ['en'];
|
||||
</script>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ClassyProvider } from '../ts/components/PopperRootContext';
|
|||
import { I18n } from '../sticker-creator/util/i18n';
|
||||
import { StorybookThemeContext } from './StorybookThemeContext';
|
||||
import { ThemeType } from '../ts/types/Util';
|
||||
import { setupI18n } from '../ts/util/setupI18n';
|
||||
|
||||
export const globalTypes = {
|
||||
mode: {
|
||||
|
@ -37,6 +38,8 @@ export const globalTypes = {
|
|||
},
|
||||
};
|
||||
|
||||
window.i18n = setupI18n('en', messages);
|
||||
|
||||
const withModeAndThemeProvider = (Story, context) => {
|
||||
const theme =
|
||||
context.globals.theme === 'light' ? ThemeType.light : ThemeType.dark;
|
||||
|
|
|
@ -2579,6 +2579,18 @@
|
|||
"message": "Recently used stickers will appear here.",
|
||||
"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": {
|
||||
"message": "Sticker Pack",
|
||||
"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.",
|
||||
"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": {
|
||||
"message": "Draw",
|
||||
"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';
|
||||
src: url('../fonts/stories/EBGaramond-Regular.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hatsuishi';
|
||||
src: url('../fonts/stories/Hatsuishi-Regular.woff2');
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
// Fonts
|
||||
|
||||
@mixin font-family {
|
||||
font-family: $inter;
|
||||
@mixin localized-fonts {
|
||||
/* Japanese */
|
||||
&:lang(ja) {
|
||||
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 {
|
||||
@include font-family;
|
||||
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 {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
@ -5616,15 +5620,18 @@ button.module-image__border-overlay:focus {
|
|||
.module-sticker-picker__body {
|
||||
position: relative;
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-rows: 68px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 332px;
|
||||
height: 356px;
|
||||
padding: 8px 13px 16px 13px;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-rows: 68px;
|
||||
|
||||
&--under-text {
|
||||
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)
|
||||
|
||||
.sticker-button-wrapper {
|
||||
|
|
|
@ -27,9 +27,11 @@ import { useFabricHistory } from '../mediaEditor/useFabricHistory';
|
|||
import { usePortal } from '../hooks/usePortal';
|
||||
import { useUniqueId } from '../hooks/useUniqueId';
|
||||
|
||||
import { MediaEditorFabricPencilBrush } from '../mediaEditor/MediaEditorFabricPencilBrush';
|
||||
import { MediaEditorFabricAnalogTimeSticker } from '../mediaEditor/MediaEditorFabricAnalogTimeSticker';
|
||||
import { MediaEditorFabricCropRect } from '../mediaEditor/MediaEditorFabricCropRect';
|
||||
import { MediaEditorFabricDigitalTimeSticker } from '../mediaEditor/MediaEditorFabricDigitalTimeSticker';
|
||||
import { MediaEditorFabricIText } from '../mediaEditor/MediaEditorFabricIText';
|
||||
import { MediaEditorFabricPencilBrush } from '../mediaEditor/MediaEditorFabricPencilBrush';
|
||||
import { MediaEditorFabricSticker } from '../mediaEditor/MediaEditorFabricSticker';
|
||||
import { fabricEffectListener } from '../mediaEditor/fabricEffectListener';
|
||||
import { getRGBA, getHSL } from '../mediaEditor/util/color';
|
||||
|
@ -1062,6 +1064,53 @@ export function MediaEditor({
|
|||
fabricCanvas.setActiveObject(sticker);
|
||||
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={[]}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint={false}
|
||||
|
@ -1247,7 +1296,7 @@ function getNewImageStateFromCrop(
|
|||
|
||||
function cloneFabricCanvas(original: fabric.Canvas): Promise<fabric.Canvas> {
|
||||
return new Promise(resolve => {
|
||||
original.clone(resolve);
|
||||
original.clone(resolve, ['data']);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ export type OwnProps = {
|
|||
stickerId: number,
|
||||
url: string
|
||||
) => unknown;
|
||||
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||
readonly showIntroduction?: boolean;
|
||||
readonly clearShowIntroduction: () => unknown;
|
||||
readonly showPickerHint: boolean;
|
||||
|
@ -51,6 +52,7 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
|||
clearInstalledStickerPack,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
onPickTimeSticker,
|
||||
recentStickers,
|
||||
onOpenStateChanged,
|
||||
receivedPacks,
|
||||
|
@ -111,6 +113,14 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
|||
[setOpen, onPickSticker]
|
||||
);
|
||||
|
||||
const handlePickTimeSticker = React.useCallback(
|
||||
(style: 'analog' | 'digital') => {
|
||||
setOpen(false);
|
||||
onPickTimeSticker?.(style);
|
||||
},
|
||||
[setOpen, onPickTimeSticker]
|
||||
);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
@ -355,6 +365,9 @@ export const StickerButton = React.memo(function StickerButtonInner({
|
|||
onClickAddPack ? handleClickAddPack : undefined
|
||||
}
|
||||
onPickSticker={handlePickSticker}
|
||||
onPickTimeSticker={
|
||||
onPickTimeSticker ? handlePickTimeSticker : undefined
|
||||
}
|
||||
recentStickers={recentStickers}
|
||||
showPickerHint={showPickerHint}
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,7 @@ import FocusTrap from 'focus-trap-react';
|
|||
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||
import type { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { getAnalogTime } from '../../util/getAnalogTime';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
|
@ -18,6 +19,7 @@ export type OwnProps = {
|
|||
stickerId: number,
|
||||
url: string
|
||||
) => unknown;
|
||||
readonly onPickTimeSticker?: (style: 'analog' | 'digital') => unknown;
|
||||
readonly packs: ReadonlyArray<StickerPackType>;
|
||||
readonly recentStickers: ReadonlyArray<StickerType>;
|
||||
readonly showPickerHint?: boolean;
|
||||
|
@ -71,6 +73,7 @@ export const StickerPicker = React.memo(
|
|||
onClose,
|
||||
onClickAddPack,
|
||||
onPickSticker,
|
||||
onPickTimeSticker,
|
||||
showPickerHint,
|
||||
style,
|
||||
}: Props,
|
||||
|
@ -131,7 +134,10 @@ export const StickerPicker = React.memo(
|
|||
// Focus popup on after initial render, restore focus on teardown
|
||||
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 downloadError =
|
||||
selectedPack &&
|
||||
|
@ -142,14 +148,13 @@ export const StickerPicker = React.memo(
|
|||
? selectedPack.stickerCount - stickers.length
|
||||
: 0;
|
||||
|
||||
const hasPacks = packs.length > 0;
|
||||
const isRecents = hasPacks && currentTab === 'recents';
|
||||
const showPendingText = pendingCount > 0;
|
||||
const showDownlaodErrorText = downloadError;
|
||||
const showDownloadErrorText = downloadError;
|
||||
const showEmptyText = !downloadError && isEmpty;
|
||||
const showText =
|
||||
showPendingText || showDownlaodErrorText || showEmptyText;
|
||||
showPendingText || showDownloadErrorText || showEmptyText;
|
||||
const showLongText = showPickerHint;
|
||||
const analogTime = getAnalogTime();
|
||||
|
||||
return (
|
||||
<FocusTrap
|
||||
|
@ -303,46 +308,97 @@ export const StickerPicker = React.memo(
|
|||
</div>
|
||||
) : null}
|
||||
{!isEmpty ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-sticker-picker__body__content',
|
||||
{
|
||||
<div className="module-sticker-picker__body__content">
|
||||
{isRecents && onPickTimeSticker && (
|
||||
<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':
|
||||
showText,
|
||||
'module-sticker-picker__body__content--under-long-text':
|
||||
showLongText,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{stickers.map(({ packId, id, url }, index: number) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
})}
|
||||
>
|
||||
{stickers.map(({ packId, id, url }, index: number) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
ref={maybeFocusRef}
|
||||
key={`${packId}-${id}`}
|
||||
className="module-sticker-picker__body__cell"
|
||||
onClick={() => onPickSticker(packId, id, url)}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-picker__body__cell__image"
|
||||
src={url}
|
||||
alt={packTitle}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
ref={maybeFocusRef}
|
||||
key={`${packId}-${id}`}
|
||||
className="module-sticker-picker__body__cell"
|
||||
onClick={() => onPickSticker(packId, id, url)}
|
||||
>
|
||||
<img
|
||||
className="module-sticker-picker__body__cell__image"
|
||||
src={url}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{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}
|
||||
</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 type { ImageStateType } from './ImageStateType';
|
||||
import { MediaEditorFabricAnalogTimeSticker } from './MediaEditorFabricAnalogTimeSticker';
|
||||
import { MediaEditorFabricDigitalTimeSticker } from './MediaEditorFabricDigitalTimeSticker';
|
||||
import { MediaEditorFabricIText } from './MediaEditorFabricIText';
|
||||
import { MediaEditorFabricPath } from './MediaEditorFabricPath';
|
||||
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
|
||||
// global namespace. See <http://fabricjs.com/fabric-intro-part-3#subclassing>.
|
||||
Object.assign(fabric, {
|
||||
MediaEditorFabricAnalogTimeSticker,
|
||||
MediaEditorFabricDigitalTimeSticker,
|
||||
MediaEditorFabricIText,
|
||||
MediaEditorFabricPath,
|
||||
MediaEditorFabricSticker,
|
||||
|
@ -216,7 +220,7 @@ export function useFabricHistory({
|
|||
}
|
||||
|
||||
function getCanvasState(fabricCanvas: fabric.Canvas): string {
|
||||
return JSON.stringify(fabricCanvas.toDatalessJSON());
|
||||
return JSON.stringify(fabricCanvas.toDatalessJSON(['data']));
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|