diff --git a/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java b/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java index 1dfef1f7..61d73831 100644 --- a/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java +++ b/android/app/src/main/java/fivechan/android/FileUploaderPlugin.java @@ -3,6 +3,7 @@ package fivechan.android; import android.app.Activity; import android.content.Intent; import android.net.Uri; +import android.util.Base64; import android.util.Log; import androidx.activity.result.ActivityResult; @@ -31,6 +32,7 @@ public class FileUploaderPlugin extends Plugin { private static final String PROVIDER_CATBOX = "catbox"; private static final long CATBOX_TIMEOUT_SEC = 30; + private static final int MAX_GENERATED_UPLOAD_BYTES = 20 * 1024 * 1024; @PluginMethod public void pickAndUploadMedia(PluginCall call) { @@ -47,6 +49,62 @@ public class FileUploaderPlugin extends Plugin { startActivityForResult(call, intent, "pickFileResult"); } + @PluginMethod + public void uploadGeneratedMedia(PluginCall call) { + Log.d(TAG, "uploadGeneratedMedia called"); + List providerOrder = getProviderOrder(call); + if (providerOrder.isEmpty()) { + call.reject("No supported upload providers selected"); + return; + } + + String base64 = call.getString("base64"); + if (base64 == null || base64.trim().isEmpty()) { + call.reject("Generated upload data is required"); + return; + } + int commaIndex = base64.indexOf(','); + String base64Payload = commaIndex >= 0 ? base64.substring(commaIndex + 1) : base64; + byte[] bytes; + try { + bytes = Base64.decode(base64Payload, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + call.reject("Generated upload data is invalid"); + return; + } + if (bytes.length == 0) { + call.reject("Generated upload data is empty"); + return; + } + if (bytes.length > MAX_GENERATED_UPLOAD_BYTES) { + call.reject("Generated upload is too large"); + return; + } + + String fileName = call.getString("fileName", "tegaki.png"); + String mimeType = call.getString("mimeType", "application/octet-stream"); + new Thread( + () -> { + File cachedFile = null; + try { + cachedFile = FileUtils.writeBytesToCacheFile(getContext(), fileName, bytes); + tryProvidersSequentially(Uri.fromFile(cachedFile), providerOrder, call, fileName, bytes, mimeType); + } catch (Exception e) { + Log.e(TAG, "Generated upload failed", e); + try { + call.reject("Upload failed: " + e.getMessage()); + } catch (Exception rejectEx) { + Log.e(TAG, "Failed to reject generated upload call", rejectEx); + } + } finally { + if (cachedFile != null && cachedFile.exists() && !cachedFile.delete()) { + Log.w(TAG, "Could not delete generated upload cache file: " + cachedFile.getAbsolutePath()); + } + } + }) + .start(); + } + private List parseProviderOrder(PluginCall call) { List order = new ArrayList<>(); JSArray arr = call.getArray("providerOrder"); @@ -127,8 +185,19 @@ public class FileUploaderPlugin extends Plugin { } private void tryProvidersSequentially(Uri fileUri, List providerOrder, PluginCall call) { + tryProvidersSequentially(fileUri, providerOrder, call, null, null, null); + } + + private void tryProvidersSequentially( + Uri fileUri, + List providerOrder, + PluginCall call, + String generatedFileName, + byte[] generatedFileBytes, + String generatedMimeType) { List attempts = new ArrayList<>(); StringBuilder errorSummary = new StringBuilder(); + String fileName = generatedFileName != null ? generatedFileName : getFileName(fileUri); for (String provider : providerOrder) { JSObject attempt = new JSObject(); @@ -140,18 +209,21 @@ public class FileUploaderPlugin extends Plugin { if (res.success) { attempt.put("url", res.url); attempts.add(attempt); - resolveWithSuccess(call, res.url, getFileName(fileUri), provider, attempts); + resolveWithSuccess(call, res.url, fileName, provider, attempts); return; } attempt.put("error", res.error); errorSummary.append(provider).append(": ").append(res.error).append("; "); } else if (MediaUploadRecipes.isWebViewProvider(provider)) { - MediaUploadResult res = uploadViaWebViewSync(fileUri, provider); + MediaUploadResult res = + generatedFileBytes != null + ? uploadGeneratedViaWebViewSync(generatedFileBytes, fileName, generatedMimeType, provider) + : uploadViaWebViewSync(fileUri, provider); attempt.put("success", res.success); if (res.success) { attempt.put("url", res.url); attempts.add(attempt); - resolveWithSuccess(call, res.url, getFileName(fileUri), provider, attempts); + resolveWithSuccess(call, res.url, fileName, provider, attempts); return; } attempt.put("error", res.error); @@ -251,4 +323,54 @@ public class FileUploaderPlugin extends Plugin { return new MediaUploadResult(false, null, "Interrupted"); } } + + private MediaUploadResult uploadGeneratedViaWebViewSync( + byte[] fileBytes, String fileName, String mimeType, String provider) { + JSObject statusUpdate = new JSObject(); + statusUpdate.put("status", "Uploading to " + provider + "..."); + notifyListeners("uploadStatus", statusUpdate); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference resultRef = new AtomicReference<>(); + + AppCompatActivity activity = getActivity(); + if (activity == null) { + return new MediaUploadResult(false, null, "Activity unavailable"); + } + + MediaUploadCallback callback = + res -> { + resultRef.set(res); + latch.countDown(); + }; + + activity.runOnUiThread( + () -> { + MediaUploadAutomationRunner runner = + new MediaUploadAutomationRunner( + getContext(), + fileBytes, + fileName, + mimeType, + provider, + callback, + null); + runner.run(); + }); + + try { + boolean ok = + latch.await( + MediaUploadRecipes.getUploadTimeoutMs(provider) + 5000, + TimeUnit.MILLISECONDS); + if (!ok) { + return new MediaUploadResult(false, null, "WebView upload timeout"); + } + MediaUploadResult r = resultRef.get(); + return r != null ? r : new MediaUploadResult(false, null, "No result"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return new MediaUploadResult(false, null, "Interrupted"); + } + } } diff --git a/android/app/src/main/java/fivechan/android/FileUtils.java b/android/app/src/main/java/fivechan/android/FileUtils.java index f65cfb51..3347f0dc 100644 --- a/android/app/src/main/java/fivechan/android/FileUtils.java +++ b/android/app/src/main/java/fivechan/android/FileUtils.java @@ -16,6 +16,13 @@ public class FileUtils { private static final Pattern UNSAFE_CHARS = Pattern.compile("[^a-zA-Z0-9._-]"); public static File getFileFromUri(Context context, Uri uri) throws Exception { + if ("file".equals(uri.getScheme()) && uri.getPath() != null) { + File source = new File(uri.getPath()); + if (source.isFile()) { + return source; + } + } + String fileName = getFileName(context, uri); String sanitizedName = sanitizeFileName(fileName); File file = new File(context.getCacheDir(), sanitizedName); @@ -69,6 +76,25 @@ public class FileUtils { * Takes only the last path segment, removes/replaces unsafe chars, enforces max length, * and falls back to a UUID if the result is empty. */ + public static File writeBytesToCacheFile(Context context, String fileName, byte[] bytes) throws Exception { + String safeName = sanitizeFileName(fileName); + int dot = safeName.lastIndexOf('.'); + String base = dot > 0 ? safeName.substring(0, dot) : safeName; + String ext = dot > 0 ? safeName.substring(dot) : null; + if (base.length() < 3) { + base = (base + "___").substring(0, 3); + } + if (base.length() > 64) { + base = base.substring(0, 64); + } + File file = File.createTempFile(base + "-", ext, context.getCacheDir()); + try (FileOutputStream outputStream = new FileOutputStream(file)) { + outputStream.write(bytes); + outputStream.flush(); + } + return file; + } + private static String sanitizeFileName(String fileName) { if (fileName == null || fileName.isEmpty()) { return UUID.randomUUID().toString(); @@ -82,4 +108,4 @@ public class FileUtils { } return basename; } -} \ No newline at end of file +} diff --git a/electron/main.js b/electron/main.js index 4dc04571..d39c0eef 100644 --- a/electron/main.js +++ b/electron/main.js @@ -51,6 +51,32 @@ ipcMain.on('get-pkc-rpc-auth-key', (event) => event.reply('pkc-rpc-auth-key', pk // NOTE: eventually should probably fake sec-ch-ua header as well const fakeUserAgent = createFakeUserAgent(); const realUserAgent = `5chan/${packageJson.version}`; +const MAX_GENERATED_UPLOAD_BYTES = 20 * 1024 * 1024; +const SAFE_GENERATED_UPLOAD_NAME_RE = /[^a-zA-Z0-9._-]/g; + +const sanitizeGeneratedUploadFileName = (fileName) => { + const basename = path.basename(typeof fileName === 'string' && fileName ? fileName : 'tegaki.png').replace(/\.\./g, ''); + const sanitized = basename.replace(SAFE_GENERATED_UPLOAD_NAME_RE, '_').trim(); + return sanitized && sanitized.length <= 255 ? sanitized : 'tegaki.png'; +}; + +const writeGeneratedUploadFile = async ({ fileName, bytes }) => { + if (!Array.isArray(bytes) || bytes.length === 0) { + throw new Error('Generated upload bytes are required'); + } + if (bytes.length > MAX_GENERATED_UPLOAD_BYTES) { + throw new Error('Generated upload is too large'); + } + if (!bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) { + throw new Error('Generated upload bytes are invalid'); + } + const buffer = Buffer.from(bytes); + const safeFileName = sanitizeGeneratedUploadFileName(fileName); + const tempDir = await fs.promises.mkdtemp(path.join(app.getPath('temp'), '5chan-upload-')); + const filePath = path.join(tempDir, safeFileName); + await fs.promises.writeFile(filePath, buffer); + return { tempDir, filePath }; +}; // add right click menu contextMenu({ @@ -391,6 +417,20 @@ ipcMain.handle('automate-upload-media', async (event, options) => { return automateUploadMedia({ provider, filePath }); }); +ipcMain.handle('automate-upload-generated-media', async (event, options) => { + const { provider, fileName, bytes } = options || {}; + if (!provider) { + throw new Error('automate-upload-generated-media requires a provider'); + } + + const { tempDir, filePath } = await writeGeneratedUploadFile({ fileName, bytes }); + try { + return await automateUploadMedia({ provider, filePath }); + } finally { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + } +}); + ipcMain.handle('download-and-install-update', async (event, options) => { const { url, fileName } = options || {}; return downloadAndInstallUpdate({ url, fileName }); diff --git a/electron/preload.mjs b/electron/preload.mjs index ce280371..61a72c49 100644 --- a/electron/preload.mjs +++ b/electron/preload.mjs @@ -21,6 +21,7 @@ contextBridge.exposeInMainWorld('electronApi', { copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text), getPlatform: () => ipcRenderer.invoke('get-platform'), automateUploadMedia: (options) => ipcRenderer.invoke('automate-upload-media', options), + automateUploadGeneratedMedia: (options) => ipcRenderer.invoke('automate-upload-generated-media', options), downloadAndInstallUpdate: (options) => ipcRenderer.invoke('download-and-install-update', options), getPathForFile: (file) => { try { diff --git a/public/vendor/tegaki/0.9.4/LICENSE b/public/vendor/tegaki/0.9.4/LICENSE new file mode 100644 index 00000000..54d364f8 --- /dev/null +++ b/public/vendor/tegaki/0.9.4/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Maxime Youdine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/vendor/tegaki/0.9.4/tegaki.css b/public/vendor/tegaki/0.9.4/tegaki.css new file mode 100644 index 00000000..5e4edb47 --- /dev/null +++ b/public/vendor/tegaki/0.9.4/tegaki.css @@ -0,0 +1,669 @@ +/*! Contains fonts from Font Awesome (Copyright (C) 2016 by Dave Gandy), Entypo (Copyright (C) 2012 by Daniel Bruce) */ +@font-face { + font-family: tegaki; + src: url('data:application/octet-stream;base64,d09GRgABAAAAAAw4AAsAAAAAEpQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAQwAAAFY+IFLIY21hcAAAAYgAAAC+AAACptXj0XhnbHlmAAACSAAABtcAAAnIF+/SeGhlYWQAAAkgAAAAMwAAADYXAPWXaGhlYQAACVQAAAAgAAAAJAd1A5tobXR4AAAJdAAAAEAAAABcShf/3GxvY2EAAAm0AAAAMAAAADAZpBxobWF4cAAACeQAAAAfAAAAIAExAGtuYW1lAAAKBAAAAX4AAAK17cxnR3Bvc3QAAAuEAAAAswAAAPr4ELHkeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGS2YJzAwMrAwFTFtIeBgaEHQjM+YDBkZAKKMrAyM2AFAWmuKQwOLxg+/mYO+p/FEMUcwTANKMwIkgMA5OUMbwB4nO2SSXLCQBAEc0BsBi8sVvgRhhfxIE5+Z1115gDVUvEL90Qqenq0RWcDC2Bufk0H7Y9Gxc3VNtbnvI31jqv3a68Z6Dich/vj4YxXNkbz+cWrspmf7fyFJStXN37Plh3vfPDJF3sOHDnxTe+bl/zHri7tJ7u+ejtRJhTG7ocyp1D2FMqqgruPgj2gYCMo2A0KZVvBvlCov1OwQxRsEwV7RcGGUbBrFGwdBfv3JE14EjxJE54JhvsE/RM9XjzzAAB4nKVWXWgc1xW+597dmd2Z1ezsaHZmtZE265nZGWkt1uruaMaswtpxgyWCAomiKrFZ60FNTVuFEtuKHkLqh0JMA25cigmBpCk1GxEc8uMmqGAMJokfilvylNp6KsZJiQKpGoIfmiDN9tzZFU6bt1agc885e++5c88533cvAUK6/6Qf0zPEIaSiZ0Gw9oHIhesfAI+LoH4vhFwY+LNp0I+zs+q42umgmFX5qN61s9lOJ/uUwZXXXst+d2K2xicQQvi+XzONycRGI68Lno1CtC03nEThTfpB2EAxBXXDzDfqBtPKxmbZWDbKsGmWAI2SuYwKN/7IvZ8b6M1/3vcaZT493ocQpsE/SLa/z55+9D29mDtH42h0bTdOLwAuJRS/8adsiz1OhohLyKACIjNDFxfXDV2wLU80Ky3ALKXBhCAMPNdzReFlJZ88lV5njl+pFMXUugRHFD15//vwm+jlq/DcvpRQHBqvTUzCWFJXorX0ekosViq+w9bTp5J5pfM+XIqeuQrHx4KJ2nihKIhxrj5j63SM6ISksUIe9OpipqFXF/ZGtCSVpGhJlo/hCKMwKg9n2jKci34oy/BbuSS1ZTnaQLfclofJbl7W6WEes4IF99LQq3rIA8cx27gs2og25GH5GEZ5FUcUy21JgrHopiTx3+FViW/LQxLo7nQv0UP0Q1LEbGF+zRwWrtdSboilzNlcCzR4XpRgGdvpD7IlvaukJXP53ZRwRJToh1IqKo0O3hgYuJEzdPiR8KRoxHXYYBn6JZGxEgRKMAIiiCF2JYSuVwPby4JhGib9qFzajv62VQdrO4iOFmjdqGUUQxvWChn6evmgqm5Ht/wvobyd2dlfGK8bRVp4WNUKhV4/XmQ5Noh78By7kzk/iNOQ2LlnQNMG6N8H4KHoCVHKskDJpFDT4xR2/9V9h73EMoifKgkxUN0QmUKtGuwDIwjrge9aQl43FcAP9Vz8XjfAE5ieUKm3wK+BpYBeAiq9dSvVpod/3XnhME3MXWgOj442q9LEmYPHf5ZIdbRE6la0mTjULD8Bf5p7cXb2/Np5FCyztH/nPc2fmPC1kjA3i0uqw7rabCWkxdYzrcXx/UtRtRxk/jL+olMdrjqxiPN5iR1F7GXJvfjFOWxmjgo8cQOrlvwvmx3R1eiTrA6GAiNZI5L/w6SfbW8phqEwjctv6T38nKYTrEVyMcpLDPFnIX5aFPmEIV6sLCg0rxuN+iT2naW02zNrn67NtNsK1ao0IaiJ0XLZsQrNKgtWr6+8Mney0zk598rK9dVU9FRCEoRmbeJ4vWA9sPhSn1PepFNMISNoBEYeD4JQ8XDYC73zxHSSC0JqyYoiRytJaijT8Vm4pEnGZCG7fV1JZeBsUoieu3uy6EwyyTmhu909zTbYg2SA5y4MSlQURCH0wxaAQbjOaQALjYRAVt3FuYyz8MJ8QvAL7Mli1Fl1F1IprVS21NTB0VW4dtGSHl505rVMMSHcd5G+XoweWHEPCYpVKquCNO+uwAJRY67cxHoxIhJetUFikntwf5t4ZC/ZRxrYeVPkACGmnWsM/h//Nv7lcpO53GO53OO2fTiXczTN1rTHbJubtj3tOL5t/8T30Y+KyLTtrf/9H6EWc+wmm0NNQwx9jzTJ98ks+QFZxBr6LhZOGIHGHkvQDY6lpI14wcIa2JkBYrQGDKHmt6BeAl0B6OFNN8TdBd6u0thVBncVWK8Ufz5U+QbuxATs7Hz1TWOhUSmi7dQX3tk7U63OLEyjoNNFp9KoVC4MVfjw13h65ULR4Rx/oWcxHc3iFe7xnRbGiR7ERUOwztVMdWaeR0NBT/AQvnOgN/MK3zjW0B0937NiLu1232LHWJYznim4YY+OzD4riTFDCeKQpIIiY68yLpHiV2SFJaexiZGlUhls8CScjRV+PaygH34lJHexwn6PWBmKsWLexYrXuwQQPkHIfikpoKajU0nK4SLzTZTvwORUrPDr4Sz6+aa9+wV+TH+B2CeDeKMY/O3Qf1iEwVdKXXXdy5ddV51Qb6vqR/Skqjrj1z4Yd1T1C7Wm3ohzsIViHmMgAgZNI36deC5/liB/LOGy27jacS5fdhy1Tk/ydV/wMB9cwzAcq1H360Qd7yOGeCGVcI8IhoCpi9nNDfCQnN/gzpXo0SvsdlpQooxRjh7NW1JKUOBOvgxv5p2DN2/SI4PV7M61gqHjSO8rGLtvA963DhnDr+y9jfrvJ0C+Eb2Qc9q3X1Bp8CDP3vP9001BeFpQhGb0dn7kxDlnOg+PTPVdz/rnd34H9y9HV1lp8rz/bBOdTwvCFE6dds6dGMGpfVfztL/953jmMvk3IB776AB4nGNgZGBgAOJPRx0y4vltvjJwM78AijDcfHDjB4z+/+5/FosRcwSQy8HABBIFALlhD/oAeJxjYGRgYA76n8XAwKL//93/VyxGDEARFCAOAJQMBhd4nGN+wcDALIiEXyAwk/X/v8w8UPGJ/38wRwLFDEDiQP6h/3/gahf8/8+8gIGBMRSIQ/6/Y9H//w8kDgBfpxcoAAAAAAA8AHAAjgDSAPwBJgFSAYQBoAH6Ai4CZgKYAtIDQAPMA/wELAROBG4EnATkeJxjYGRgYBBniGcQYAABJiDmAkIGhv9gPgMAFW4BnQB4nHWQzUrDQBSFz9jaYisuLLgeN2IR0x9w0bopFlpXCl0UxIWMdZqkppkymRb6Cr6DD+EL+SyeJoMUwQwz+e65597cCYBTfEOgeG64CxaoMCr4AFXcei5Rv/NcJt97PkQdD54rXE+ea7jCi+c6GvhgB1E+YrTAp2eBY1H1fIAT0fBcon7uuUy+9nyIM9HzXKH+6LmGqXj2XMeF+Bqa1dbGYeTk5bApu+1OT75upaEUpyqRau0iYzM5kHOTOp0kJpiZpdOheo8nOlwnyhZBcU61zWKTyk7QLoSxTrVVTr/tumabsOvcXM6tWcqR7ydX1iz0zAWRc6t+q7X/HQxhsMIWFjFCRHCQuKTa5LuLNjrokV7pkHQWrhgpFBIqCmtWRHkmYzzgnjNKqWo6EnKAGc9lroSseGf9JOc184qV+5l9npJ3feO8o+QsASfad4zJae5Suf72O2uGDV1dqo4T7aay+RQSoz/zSd5/l1tQmVEP8r/gqPbR4vrnPj+rn3wZAAB4nG2NWVLDMBBE1YktLySBbIRL+FCyMiYqFEk1GuHi9iT4l/fTS1VXq5Va6NX/XLDCGhVqaDRo0aHHCzbYYodXvGGPA4444Yx3XPChtDXBkq+SL7m+u1DyOlHornEOQ3y4pqQ/7T19kx+evX5E654T86PHYr9I6mRKpmr0hVuJQxbD0s9GiG30kSuJgVrjeOSSb81ksgzTvOg4d54mWU7Yfd4Wq4lNJm6SSyRCSv0C/DA9cAA=') format('woff'); + font-weight: normal; + font-style: normal; +} + +.tegaki-icon:before { + font-family: tegaki; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; +} + +.tegaki-cancel:before { content: '\e800'; } /* '' */ +.tegaki-plus:before { content: '\e801'; } /* '' */ +.tegaki-minus:before { content: '\e802'; } /* '' */ +.tegaki-pen:before { content: '\e803'; } /* '' */ +.tegaki-down-open:before { content: '\e804'; } /* '' */ +.tegaki-up-open:before { content: '\e805'; } /* '' */ +.tegaki-level-down:before { content: '\e806'; } /* '' */ +.tegaki-pencil:before { content: '\e807'; } /* '' */ +.tegaki-play:before { content: '\e808'; } /* '' */ +.tegaki-bucket:before { content: '\e809'; } /* '' */ +.tegaki-pause:before { content: '\e80a'; } /* '' */ +.tegaki-blur:before { content: '\e80b'; } /* '' */ +.tegaki-to-start:before { content: '\e80c'; } /* '' */ +.tegaki-watercolor:before { content: '\e80d'; } /* '' */ +.tegaki-tone:before { content: '\e80e'; } /* '' */ +.tegaki-airbrush:before { content: '\e80f'; } /* '' */ +.tegaki-fast-fw:before { content: '\e810'; } /* '' */ +.tegaki-fast-bw:before { content: '\e811'; } /* '' */ +.tegaki-left-open:before { content: '\e812'; } /* '' */ +.tegaki-right-open:before { content: '\e813'; } /* '' */ +.tegaki-eraser:before { content: '\f12d'; } /* '' */ +.tegaki-pipette:before { content: '\f1fb'; } /* '' */ + +.tegaki-disabled, +.tegaki-disabled::after, +.tegaki-disabled::before { + opacity: 0.45; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-invis { + visibility: hidden !important; + width: 0 !important; + height: 0 !important; +} + +.tegaki-replay-mode #tegaki-tools-cnt, +.tegaki-replay-mode #tegaki-toolmode-bar, +.tegaki-replay-mode .tegaki-ctrlgrp, +.tegaki-replay-mode .tegaki-layers-cell, +.tegaki-replay-mode #tegaki-layers-ctrl { + pointer-events: none; +} + +.tegaki-replay-mode #tegaki-ctrlgrp-zoom, +.tegaki-replay-mode #tegaki-ctrlgrp-layers { + pointer-events: auto; +} + +#tegaki { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: #a3b1bf; + color: #222; + font-family: arial, sans-serif; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 9999; + display: grid; + grid-template-columns: 40px 1fr 160px; + grid-template-rows: 24px 1fr 18px; + gap: 2px; +} + +#tegaki input { + color: inherit; +} + +#tegaki > div { + background-color: #8d99a6; +} + +#tegaki-menu-cnt { + grid-area: 1 / 1 / 2 / 4; + white-space: nowrap; + overflow: hidden; + display: flex; +} + +#tegaki-tools-cnt { + grid-area: 2 / 1 / 4 / 2; + padding: 4px; +} + +#tegaki-canvas-cnt { + grid-area: 2 / 2 / 3 / 3; + overflow: auto; + display: flex; +} + +#tegaki-ctrl-cnt { + grid-area: 2 / 3 / 4 / 4; + padding: 6px; + overflow: hidden auto; +} + +#tegaki-status-cnt { + grid-area: 3 / 2 / 4 / 3; + line-height: 18px; + display: flex; +} + +#tegaki-status-cnt > div { + padding: 0 4px; +} + +#tegaki-status-replay { + color: #a61930; +} + +#tegaki-status-output { + font-size: 11px; + font-weight: bold; +} + +#tegaki-status-version { + color: #adbdcc; + font-size: 11px; + margin-left: auto; +} + +#tegaki-menu-bar { + font-size: 12px; + padding-left: 4px; + padding-right: 18px; + border-right: 2px solid #a3b1bf; +} + +.tegaki-replay-mode #tegaki-menu-bar { + padding-right: 4px; +} + +.tegaki-menu-lbl { + margin: 0 2px; + vertical-align: middle; +} + +#tegaki-replay-controls { + padding-right: 10px; + padding-left: 10px; + border-right: 2px solid #a3b1bf; + font-size: 11px; +} + +#tegaki-replay-timeline { + display: inline-block; + width: 100px; + height: 24px; + margin: 0 4px; + border-left: 1px solid #a3b1bf; + border-right: 1px solid #a3b1bf; + /* background-color: rgba(0, 0, 0, 0.25); */ +} + +#tegaki-replay-timeline-fill { + display: inline-block; + width: 36px; + height: 100%; + background-color: #a3b1bf; +} + +#tegaki-replay-controls > span { + vertical-align: middle; +} + +#tegaki-replay-controls .tegaki-ui-cb-w { + margin-right: 4px; +} + +#tegaki-replay-speed-lbl { + width: 28px; + display: inline-block; + text-align: center; +} + +#tegaki-replay-speed-lbl::before { + content: '×'; +} + +#tegaki-replay-now-lbl, +#tegaki-replay-end-lbl { + display: inline-block; + max-width: 50px; + min-width: 30px; + overflow: hidden; + text-align: center; + margin: 0 4px; +} + +#tegaki-toolmode-bar { + font-size: 11px; + margin-left: 4px; + line-height: 24px; +} + +.tegaki-toolmode-lbl { + margin-right: 6px; +} + +.tegaki-toolmode-lbl::after { + content: ':'; +} + +.tegaki-toolmode-grp { + border-left: 1px solid #a3b1bf; + padding: 0 18px; +} + +#tegaki canvas { + image-rendering: optimizespeed; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-optimize-contrast; + image-rendering: pixelated; + -ms-interpolation-mode: nearest-neighbor; +} + +.tegaki-tool-active { + color: #f2f3f4; +} + +.tegaki-tool-btn { + width: 32px; + height: 32px; + display: block; + margin: auto; +} + +.tegaki-tool-btn:hover { + background-color: rgba(0, 0, 0, 0.15); +} + +.tegaki-tool-btn:before { + font-size: 20px; + width: 32px; + height: 32px; + line-height: 32px; +} + +.tegaki-mb-btn { + cursor: default; + text-decoration: none; + display: inline-block; + padding: 0 6px; + word-spacing: -1px; + position: relative; + line-height: 24px; + height: 24px; +} + +.tegaki-mb-btn:hover:not(.tegaki-disabled), +.tegaki-ui-btn:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.10); +} + +.tegaki-sw-btn { + display: inline; + padding: 2px 6px; + margin: 0 2px; + box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.15), -1px -1px 0 rgba(255, 255, 255, 0.15); +} + +.tegaki-sw-btn:hover:not(.tegaki-sw-btn-a) { + background-color: rgba(0, 0, 0, 0.05); +} + +.tegaki-sw-btn-a { + background-color: rgba(0, 0, 0, 0.1); + box-shadow: -1px -1px 0 rgba(0, 0, 0, 0.15), 1px 1px 0 rgba(255, 255, 255, 0.15); +} + +#tegaki-toolmode-bar .tegaki-mb-btn-a { + color: inherit; + background-color: rgba(0, 0, 0, 0.10); +} + +#tegaki-toolmode-bar .tegaki-mb-btn.tegaki-mb-btn-a:hover { + color: inherit; +} + +#tegaki-debug { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-debug canvas { + width: 75px; + height: 75px; + display: block; + border: 1px solid black; +} + +.tegaki-backdrop { + overflow: hidden; +} + +.tegaki-hidden { + display: none !important; +} + +.tegaki-strike { + text-decoration: line-through; +} + +#tegaki-layers { + position: relative; + font-size: 0; + box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.25); + contain: content; +} + +#tegaki-layers canvas { + width: 100%; + height: 100%; +} + +#tegaki-layers:empty { + display: none; +} + +#tegaki-layers-wrap { + margin: auto; + padding: 50px; + pointer-events: none; +} + +#tegaki-layers canvas { + position: absolute; + left: 0; + top: 0; +} + +#tegaki-finish-btn { + font-weight: bold; +} + +#tegaki-cursor-layer { + position: absolute; + mix-blend-mode: difference; + touch-action: none; +} + +/* generic ui */ +.tegaki-alpha-bg, +.tegaki-alpha-bg-xs { + background-color: #fefefe; + background-image: + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca), + linear-gradient(45deg, #cacaca 25%, transparent 25%, transparent 75%, #cacaca 75%, #cacaca); +} + +.tegaki-alpha-bg { + background-size: 16px 16px; + background-position: 0 0, 8px 8px; +} + +.tegaki-alpha-bg-xs { + background-size: 6px 6px; + background-position: 0 0, 3px 3px; +} + +.tegaki-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.tegaki-ui-cb { + display: inline-block; + vertical-align: middle; + width: 10px; + height: 10px; + border: 1px solid #222; + cursor: default; + margin-right: 4px; +} + +.tegaki-ui-cb::after, +.tegaki-ui-cb-a::after { + display: block; + content: ' '; + width: 6px; + height: 6px; + margin-top: 2px; + margin-left: 2px; +} + +.tegaki-ui-cb-a::after { + background-color: #222; +} + +.tegaki-ui-cb-w:hover .tegaki-ui-cb::after, +.tegaki-ui-cb:hover::after { + background-color: #555; +} + +.tegaki-ui-ellipsis::after { + content: '...'; + letter-spacing: -1px; +} + +.tegaki-ui-borderless { + border: none; +} + +.tegaki-ui-btn { + display: inline-block; +} + +.tegaki-ui-btn:before { + height: 24px; + width: 24px; + line-height: 24px; + font-size: 14px; +} + +.tegaki-stealth-input { + border: 0; + margin: 0; + padding: 0; + background: none; +} + +.tegaki-stealth-input:hover:not(.tegaki-disabled) { + background-color: rgba(0, 0, 0, 0.1); +} + +.tegaki-range-lbl, +.tegaki-range-lbl-xs { + display: inline-block; + text-align: center; + vertical-align: top; +} + +.tegaki-range-lbl { + width: 28px; + font-size: 12px; + margin-left: 4px; +} + +.tegaki-range-lbl-xs { + width: 20px; + font-size: 10px; +} + +.tegaki-label-xs { + font-size: 10px; + vertical-align: top; +} + +.tegaki-lbl-c::after { + content: ':'; +} + +.tegaki-lbl-p::after { + content: '%'; + margin-left: 1px; +} + +.tegaki-drag-lbl:not(.tegaki-disabled) { + cursor: ew-resize; +} + +.tegaki-disabled .tegaki-drag-lbl { + cursor: auto; +} + +/* control groups */ +.tegaki-ctrlgrp { + margin-bottom: 10px; +} + +.tegaki-ctrlgrp:last-child { + margin-bottom: 0; +} + +.tegaki-ctrlgrp-title { + font-size: 12px; + font-weight: bold; + margin-bottom: 6px; + background-color: #a3b1bf; + padding: 1px 4px; +} + +.tegaki-ctrlrow { + font-size: 11px; +} + +.tegaki-ctrlrow:not(:last-child) { + margin-bottom: 6px; +} + +.tegaki-ctrl-range { + width: calc(100% - 34px); + padding: 0; + margin: 0; + height: 14px; +} + +/* zoom ctrl group */ +#tegaki-zoom-lbl { + display: inline-block; + font-size: 12px; + float: right; + height: 24px; + line-height: 24px; +} + +/* color ctrl group */ +#tegaki-color-ctrl { + display: flex; +} + +#tegaki-palette-switcher { + align-self: center; + margin-left: auto; +} + +.tegaki-color-grid { + display: grid; + gap: 4px; + margin-top: 6px; +} + +.tegaki-color-grid-20 { + grid-template-columns: repeat(auto-fill, 20px); + grid-auto-rows: 20px; +} + +.tegaki-color-grid-15 { + grid-template-columns: repeat(auto-fill, 15px); + grid-auto-rows: 15px; +} + +.tegaki-color-btn { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); +} + +#tegaki-color, +#tegaki-colorpicker { + padding: 0; + border: 0; + display: block; + width: 28px; + height: 28px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); +} + +/* layers ctrl group */ +#tegaki-ctrlgrp-layers { + position: relative; +} + +#tegaki-layers-opts { + height: 18px; + display: flex; +} + +#tegaki-layer-alpha-cell { + margin-left: auto; +} + +#tegaki-layers-ctrl { + margin-top: 4px; +} + +#tegaki-layers-grid { + height: 84px; + min-height: 84px; + overflow: auto; + background-color: #8d99a6; + display: flex; + flex-direction: column; + border: 1px solid #a3b1bf; + resize: vertical; +} + +.tegaki-layers-cell { + box-sizing: border-box; + box-shadow: 0 1px 0 0px #a3b1bf; + padding: 0; + height: 28px; + flex-shrink: 0; + overflow: hidden; + display: flex; + align-items: center; +} + +.tegaki-layers-cell-s, +.tegaki-layers-cell-a { + background-color: #a3b1bf7f; +} + +.tegaki-layers-cell-a { + font-weight: bold; +} + +.tegaki-layers-cell-v { + margin: 0 6px 0 4px; +} + +.tegaki-layers-cell-v .tegaki-ui-cb { + vertical-align: unset; + margin: 0; +} + +.tegaki-layers-cell-p { + margin-right: 6px; +} + +.tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); + vertical-align: middle; +} + +.tegaki-layers-cell-s .tegaki-layers-cell-p canvas { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 1.0); +} + +.tegaki-layers-cell-n { + font-size: 11px; + margin-right: 1px; + min-width: 20px; +} + +.tegaki-layers-cell-d { + box-shadow: inset 0 -2px 0 0 #000; + z-index: 2; +} + +#tegaki-layers-grid.tegaki-layers-cell-d { + box-shadow: 0 -2px 0 0px #000; +} + +#tegaki-layers-cell-dx { + position: absolute; + background: transparent; + width: 100%; + height: 32px; + margin-top: -32px; +} diff --git a/public/vendor/tegaki/0.9.4/tegaki.min.js b/public/vendor/tegaki/0.9.4/tegaki.min.js new file mode 100644 index 00000000..0616a9dc --- /dev/null +++ b/public/vendor/tegaki/0.9.4/tegaki.min.js @@ -0,0 +1,5 @@ +/*! tegaki.js, MIT License */"use strict";var TegakiStrings={badDimensions:"Invalid dimensions.",promptWidth:"Canvas width in pixels",promptHeight:"Canvas height in pixels",confirmDelLayers:"Delete selected layers?",confirmMergeLayers:"Merge selected layers?",tooManyLayers:"Layer limit reached.",errorLoadImage:"Could not load the image.",noActiveLayer:"No active layer.",hiddenActiveLayer:"The active layer is not visible.",confirmCancel:"Are you sure? Your work will be lost.",confirmChangeCanvas:"Are you sure? Changing the canvas will clear all layers and history and disable replay recording.",color:"Color",size:"Size",alpha:"Opacity",flow:"Flow",zoom:"Zoom",layers:"Layers",switchPalette:"Switch color palette",paletteSlotReplace:"Right click to replace with the current color",layer:"Layer",addLayer:"Add layer",delLayers:"Delete layers",mergeLayers:"Merge layers",moveLayerUp:"Move up",moveLayerDown:"Move down",toggleVisibility:"Toggle visibility",newCanvas:"New",open:"Open",save:"Save",saveAs:"Save As","export":"Export",undo:"Undo",redo:"Redo",close:"Close",finish:"Finish",tip:"Tip",pressure:"Pressure",preserveAlpha:"Preserve Alpha",pen:"Pen",pencil:"Pencil",airbrush:"Airbrush",pipette:"Pipette",blur:"Blur",eraser:"Eraser",bucket:"Bucket",tone:"Tone",gapless:"Gapless",play:"Play",pause:"Pause",rewind:"Rewind",slower:"Slower",faster:"Faster",recordingEnabled:"Recording replay",errorLoadReplay:"Could not load the replay: ",loadingReplay:"Loading replay\u2026"};class TegakiTool{constructor(){this.id=0,this.name=null,this.keybind=null,this.useFlow=!1,this.useSizeDynamics=!1,this.useAlphaDynamics=!1,this.useFlowDynamics=!1,this.usePreserveAlpha=!1,this.step=0,this.size=1,this.alpha=1,this.flow=1,this.useSize=!0,this.useAlpha=!0,this.useFlow=!0,this.noCursor=!1,this.color="#000000",this.rgb=[0,0,0],this.brushSize=0,this.brushAlpha=0,this.brushFlow=0,this.stepSize=0,this.center=0,this.sizeDynamicsEnabled=!1,this.alphaDynamicsEnabled=!1,this.flowDynamicsEnabled=!1,this.preserveAlphaEnabled=!1,this.tip=-1,this.tipList=null,this.stepAcc=0,this.shapeCache=null,this.kernel=null}brushFn(e,t,a,i){}start(e,t){}commit(){}draw(e,t){}usesDynamics(){return this.useSizeDynamics||this.useAlphaDynamics||this.useFlowDynamics}enabledDynamics(){return this.sizeDynamicsEnabled||this.alphaDynamicsEnabled||this.flowDynamicsEnabled}setSize(e){this.size=e}setAlpha(e){this.alpha=e,this.brushAlpha=e}setFlow(e){this.flow=e,this.brushFlow=this.easeFlow(e)}easeFlow(e){return e}setColor(e){this.rgb=$T.hexToRgb(e)}setSizeDynamics(e){this.useSizeDynamics&&(e||this.setSize(this.size),this.sizeDynamicsEnabled=e)}setAlphaDynamics(e){this.useAlphaDynamics&&(e||this.setAlpha(this.alpha),this.alphaDynamicsEnabled=e)}setFlowDynamics(e){this.useFlowDynamics&&(e||this.setFlow(this.flow),this.flowDynamicsEnabled=e)}setPreserveAlpha(e){this.preserveAlphaEnabled=e}set(){this.setAlpha(this.alpha),this.setFlow(this.flow),this.setSize(this.size),this.setColor(Tegaki.toolColor),Tegaki.onToolChanged(this)}}class TegakiBrush extends TegakiTool{constructor(){super()}generateShape(e){}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y,f,v,m,b,L,C,w,S,I,U,P,A,D,E,$;for($=this.preserveAlphaEnabled,g=this.kernel,D=this.brushAlpha,E=this.brushFlow,A=this.brushSize,s=Tegaki.activeLayer.imageData.data,r=Tegaki.ghostBuffer.data,n=Tegaki.blendBuffer.data,o=Tegaki.baseWidth,l=Tegaki.baseHeight,o,f=this.rgb[0],v=this.rgb[1],m=this.rgb[2],c=0;c=l))for(d=0;d=o||(T=g[4*(c*A+d)+3]/255)<=0||(y=n[(U=4*(p*o+h))+3]/255,y+=T*E*(D-y),(P=Math.ceil(255*y))>n[U+3]&&(0===n[U]&&(r[U]=s[U],r[U+1]=s[U+1],r[U+2]=s[U+2],r[U+3]=s[U+3]),n[U]=1,n[U+3]=P,w=r[U],S=r[U+1],I=r[U+2],b=(f*y+w*(k=r[U+3]/255)*(1-y))/(u=k+y-k*y),L=(v*y+S*k*(1-y))/u,C=(m*y+I*k*(1-y))/u,s[U]=f>w?Math.ceil(b):Math.floor(b),s[U+1]=v>S?Math.ceil(L):Math.floor(L),s[U+2]=m>I?Math.ceil(C):Math.floor(C),$||(s[U+3]=Math.ceil(255*u))))}generateShapeCache(e){var t,a;if(this.shapeCache||(this.shapeCache=new Array(Tegaki.maxSize)),!this.shapeCache[0]||e)for(t=0;tg?l:0!==g?-g:0)/2,0!==l&&(l=-l),m=!1,p=s,k=r;h+=Math.max(Math.abs(p-s),Math.abs(k-r)),p=s,k=r,h>=this.stepSize&&(this.enabledDynamics()?(v=T>0?1-Math.sqrt((e-s)*(e-s)+(t-r)*(t-r))/T:0,this.updateDynamics(v)&&(this.brushFn(s-this.center-n,r-this.center-o,n,o),m=!0)):(this.brushFn(s-this.center-n,r-this.center-o,n,o),m=!0),h=0),s!==e||r!==t;)(c=d)>l&&(d-=g,s+=a),c=i;)r[n+a-g+(n+i-g)*e]=o,r[n+i-g+(n+a-g)*e]=o,r[n-i+(n+a-g)*e]=o,r[n-a+(n+i-g)*e]=o,r[n-i+(n-a)*e]=o,r[n-a+(n-i)*e]=o,r[n+i-g+(n-a)*e]=o,r[n+a-g+(n-i)*e]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;return l>0&&Tegaki.tools.bucket.fill(s,l,l,this.rgb,1),{center:l,stepSize:Math.ceil(e*this.step),brushSize:e,kernel:s.data}}}class TegakiAirbrush extends TegakiBrush{constructor(){super(),this.id=3,this.name="airbrush",this.keybind="a",this.step=.1,this.size=32,this.alpha=1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.useFlowDynamics=!0,this.usePreserveAlpha=!0}easeFlow(e){return 1-Math.sqrt(1-e)}generateShape(e){var t,a,i,s,r,n,o,l,g,d,c,h;for(a=e,e*=2,i=new ImageData(e,e).data,s=e*e*4,n=Math.sqrt(a*a),l=g=-(o=Math.round(a)),t=0;t=o?(l=-o,++g):(c=g,(d=l)<0&&(d=-d),c<0&&(c=-c),(r=Math.sqrt(d*d+c*c))>=n?h=0:0===r?h=255:((h=r/n+.1)>1&&(h=1),h=255*(1-Math.exp(1-1/h)/h)),i[t+3]=h,t+=4,++l);return{center:a,stepSize:Math.ceil(e*this.step),brushSize:e,kernel:i}}}class TegakiPen extends TegakiBrush{constructor(){super(),this.id=2,this.name="pen",this.keybind="p",this.step=.05,this.size=8,this.alpha=1,this.flow=1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.useFlowDynamics=!0,this.usePreserveAlpha=!0}easeFlow(e){return 1-Math.sqrt(1-Math.pow(e,3))}generateShape(e){var t,a,i,s,r,n,o,l,g,d,c,h,p,k,T,u,y,f;for(y=Math.floor(e/2)+1,c=(d=4)*d,h=(f=e+2)*d,l=Math.floor(h/2),g=Math.floor((h+1)%2),s=new ImageData(h,h),p=new Uint32Array(s.data.buffer),o=1426063360,a=l,i=0,t=1-l,n=l;a>=i;)p[n+a-g+(n+i-g)*h]=o,p[n+i-g+(n+a-g)*h]=o,p[n-i+(n+a-g)*h]=o,p[n-a+(n+i-g)*h]=o,p[n-i+(n-a)*h]=o,p[n-a+(n-i)*h]=o,p[n+i-g+(n-a)*h]=o,p[n+a-g+(n-i)*h]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;for(o=4278190080,a=l-3,i=0,t=1-l,n=l;a>=i;)p[n+a-g+(n+i-g)*h]=o,p[n+i-g+(n+a-g)*h]=o,p[n-i+(n+a-g)*h]=o,p[n-a+(n+i-g)*h]=o,p[n-i+(n-a)*h]=o,p[n-a+(n-i)*h]=o,p[n+i-g+(n-a)*h]=o,p[n+a-g+(n-i)*h]=o,++i,t+=t<=0?2*i+1:2*(i- --a)+1;for(l>0&&Tegaki.tools.bucket.fill(s,l,l,this.rgb,1),p=s.data,r=new ImageData(f,f).data,a=0;a=0&&(l=4*(v+L),this.testPixel(C,l,k,g,d,c,h));)this.blendPixel(C,l,r,n,o,s),k[l]=1,y>=0&&(l=4*(m+L),this.testPixel(C,l,k,g,d,c,h)&&(p.push(L),p.push(y))),f=0&&(l=4*(m+L),this.testPixel(C,l,k,g,d,c,h)&&(p.push(L),p.push(y))),f=Tegaki.baseWidth||t>=Tegaki.baseHeight||(this.fill(Tegaki.activeLayer.imageData,e,t,this.rgb,this.alpha),Tegaki.activeLayer.ctx.putImageData(Tegaki.activeLayer.imageData,0,0))}blendPixel(e,t,a,i,s,r){var n,o,l,g,d,c,h,p;n=e[t],o=e[t+1],l=e[t+2],d=(a*r+n*(g=e[t+3]/255)*(1-r))/(p=g+r-g*r),c=(i*r+o*g*(1-r))/p,h=(s*r+l*g*(1-r))/p,e[t]=a>n?Math.ceil(d):Math.floor(d),e[t+1]=i>o?Math.ceil(c):Math.floor(c),e[t+2]=s>l?Math.ceil(h):Math.floor(h),e[t+3]=Math.ceil(255*p)}testPixel(e,t,a,i,s,r,n){return!a[t]&&e[t]==i&&e[++t]==s&&e[++t]==r&&e[++t]==n}start(e,t){this.brushFn(e,t)}draw(e,t){this.brushFn(e,t)}setSize(e){}}class TegakiTone extends TegakiPencil{constructor(){super(),this.id=5,this.name="tone",this.keybind="t",this.step=.01,this.useFlow=!1,this.size=8,this.alpha=.5,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.usePreserveAlpha=!0,this.matrix=[[0,8,2,10],[12,4,14,6],[3,11,1,9],[15,7,13,5]],this.mapCache=null,this.mapWidth=0,this.mapHeight=0}start(e,t){this.mapWidth===Tegaki.baseWidth&&this.mapHeight===Tegaki.baseHeight||this.generateMapCache(!0),super.start(e,t)}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y;if(s=Tegaki.activeLayer.imageData.data,u=Tegaki.baseWidth,y=Tegaki.baseHeight,r=this.kernel,n=this.brushSize,this.mapWidth,g=this.preserveAlphaEnabled,!((l=Math.round(16*this.brushAlpha)-1)<0))for(o=this.mapCache[l],p=0;p=y))for(h=0;h=u||0!==r[4*(p*n+h)+3]&&(d=4*(c=T*u+k),0===o[c]&&(s[d]=this.rgb[0],s[d+1]=this.rgb[1],s[d+2]=this.rgb[2],g||(s[d+3]=255)))}generateMap(e,t,a){var i,s,r;for(i=new Uint8Array(e*t),r=0;r=T))for(f=0;f=u||(S=4*(k*T+p),0===d[4*(f*r+y)+3]||p<=0||k<=0||p>=l||k>=g))){for(v=m=b=L=U=0,C=-1;C<2;++C)for(w=-1;w<2;++w)I=n[(s=4*((k-w)*T+(p-C)))+3],0===C&&0===w?(P=I*h,U+=h):(P=I*c,U+=c),v+=n[s]*P,m+=n[++s]*P,b+=n[++s]*P,L+=P;(L/=U)<=0||(o[S]=Math.round(v/U/L),o[S+1]=Math.round(m/U/L),o[S+2]=Math.round(b/U/L),o[S+3]=Math.round(L))}}}TegakiBlur.prototype.generateShape=TegakiPencil.prototype.generateShape;class TegakiEraser extends TegakiBrush{constructor(){super(),this.id=8,this.name="eraser",this.keybind="e",this.step=.1,this.size=8,this.alpha=1,this.useFlow=!1,this.useSizeDynamics=!0,this.useAlphaDynamics=!0,this.usePreserveAlpha=!1,this.tipId=0,this.tipList=["pencil","pen","airbrush"]}brushFn(e,t,a,i){var s,r,n,o,l,g,d,c,h,p,k,T,u,y,f;for(f=this.brushAlpha,y=this.brushSize,o=this.kernel,s=Tegaki.activeLayer.imageData.data,n=Tegaki.ghostBuffer.data,r=Tegaki.blendBuffer.data,l=Tegaki.baseWidth,g=Tegaki.baseHeight,k=0;k=g))for(p=0;p=l||(d=o[4*(k*y+p)+3]/255,0===n[h=4*(u*l+T)+3]&&(n[h]=s[h]),c=r[h]/255,c+=d*(f-c),r[h]=Math.floor(255*c),s[h]=Math.floor(n[h]*(1-c)))}generateShape(e){return 0===this.tipId?this.generateShapePencil(e):1===this.tipId?this.generateShapePen(e):this.generateShapeAirbrush(e)}}TegakiEraser.prototype.generateShapePencil=TegakiPencil.prototype.generateShape,TegakiEraser.prototype.generateShapePen=TegakiPen.prototype.generateShape,TegakiEraser.prototype.generateShapeAirbrush=TegakiAirbrush.prototype.generateShape;var $T={docEl:document.documentElement,id:function(e){return document.getElementById(e)},cls:function(e,t){return(t||document).getElementsByClassName(e)},on:function(e,t,a){e.addEventListener(t,a,!1)},off:function(e,t,a){e.removeEventListener(t,a,!1)},el:function(e){return document.createElement(e)},frag:function(){return document.createDocumentFragment()},copyImageData:e=>new ImageData(new Uint8ClampedArray(e.data),e.width),copyCanvas:function(e,t){var a;return t?a=e.cloneNode(!1):((a=$T.el("canvas")).width=e.width,a.height=e.height),a.getContext("2d").drawImage(e,0,0),a},clearCtx:function(e){e.clearRect(0,0,e.canvas.width,e.canvas.height)},hexToRgb:function(e){var t=e.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i);return t?[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]:null},RgbToHex:function(e,t,a){return"#"+((1<<24)+(e<<16)+(t<<8)+a).toString(16).slice(1)},getColorAt:function(e,t,a){var i=e.getImageData(t,a,1,1).data;return"#"+("0"+i[0].toString(16)).slice(-2)+("0"+i[1].toString(16)).slice(-2)+("0"+i[2].toString(16)).slice(-2)},generateFilename:function(){return"tegaki_"+(new Date).toISOString().split(".")[0].replace(/[^0-9]/g,"_")},sortAscCb:function(e,t){return e>t?1:et?-1:ea&&(i=a/e,e=a,t*=i),t>a&&(i=a/t,t=a,e*=i),[Math.ceil(e),Math.ceil(t)]}};class TegakiBinReader{constructor(e){this.pos=0,this.view=new DataView(e),this.buf=e}readInt8(){var e=this.view.getInt8(this.pos);return this.pos+=1,e}readUint8(){var e=this.view.getUint8(this.pos);return this.pos+=1,e}readInt16(){var e=this.view.getInt16(this.pos);return this.pos+=2,e}readUint16(){var e=this.view.getUint16(this.pos);return this.pos+=2,e}readUint32(){var e=this.view.getUint32(this.pos);return this.pos+=4,e}readFloat32(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e}}class TegakiBinWriter{constructor(e){this.pos=0,this.view=new DataView(e),this.buf=e}writeInt8(e){this.view.setInt8(this.pos,e),this.pos+=1}writeUint8(e){this.view.setUint8(this.pos,e),this.pos+=1}writeInt16(e){this.view.setInt16(this.pos,e),this.pos+=2}writeUint16(e){this.view.setUint16(this.pos,e),this.pos+=2}writeUint32(e){this.view.setUint32(this.pos,e),this.pos+=4}writeFloat32(e){this.view.setFloat32(this.pos,e),this.pos+=4}}var TegakiCursor={size:0,radius:0,points:null,cursorCtx:null,offsetX:0,offsetY:0,lastX:0,lastY:0,lastSize:0,init:function(){var e;(e=$T.el("canvas")).id="tegaki-cursor-layer",[e.width,e.height]=TegakiCursor.getMaxCanvasSize(),Tegaki.canvasCnt.appendChild(e),this.offsetX=e.offsetLeft,this.offsetY=e.offsetTop,this.cursorCtx=e.getContext("2d")},getCanvas:function(){return this.cursorCtx?this.cursorCtx.canvas:null},getMaxCanvasSize:function(){let e=Tegaki.canvasCnt.offsetWidth,t=Tegaki.canvasCnt.offsetHeight,[a,i]=Tegaki.getScrollbarSizes();return[e-a,t-i]},updateCanvasSize:function(){let e=this.cursorCtx.canvas,[t,a]=this.getMaxCanvasSize();t===e.width&&a===e.height||(e.width=t,e.height=a,this.offsetX=e.offsetLeft,this.offsetY=e.offsetTop)},render:function(e,t){var a,i,s,r,n;for(a=0|e-this.offsetX-this.radius,i=0|t-this.offsetY-this.radius,this.clear(),this.lastX=a,this.lastY=i,this.lastSize=this.size,r=this.cursorCtx.createImageData(this.size,this.size),n=new Uint32Array(r.data.buffer),s=0;s=i;)o.push(s+a-n+(s+i-n)*e),o.push(s+i-n+(s+a-n)*e),o.push(s-i+(s+a-n)*e),o.push(s-a+(s+i-n)*e),o.push(s-i+(s-a)*e),o.push(s-a+(s-i)*e),o.push(s+i-n+(s-a)*e),o.push(s+a-n+(s-i)*e),++i,t+=t<=0?2*i+1:2*(i- --a)+1;return this.size=e,this.radius=r,this.points=o,!0}},TegakiHistory={maxSize:50,undoStack:[],redoStack:[],pendingAction:null,clear:function(){this.undoStack=[],this.redoStack=[],this.pendingAction=null,this.onChange(0)},push:function(e){e&&(e.coalesce&&this.undoStack[this.undoStack.length-1]instanceof e.constructor&&this.undoStack[this.undoStack.length-1].coalesce(e)||(this.undoStack.push(e),this.undoStack.length>this.maxSize&&this.undoStack.shift(),this.redoStack.length>0&&(this.redoStack=[]),this.onChange(0)))},undo:function(){var e;this.undoStack.length&&((e=this.undoStack.pop()).undo(),this.redoStack.push(e),this.onChange(-1))},redo:function(){var e;this.redoStack.length&&((e=this.redoStack.pop()).redo(),this.undoStack.push(e),this.onChange(1))},onChange:function(e){Tegaki.onHistoryChange(this.undoStack.length,this.redoStack.length,e)},sortPosLayerCallback:function(e,t){return e[0]>t[0]?1:e[0]=0;e--)t=Tegaki.layers[e],Tegaki.layersCnt.insertBefore(t.canvas,a.nextElementSibling);TegakiLayers.setActiveLayer(this.aLayerId)},TegakiHistoryActions.MoveLayers.prototype.redo=function(){var e,t=new Set;for(e of this.layers.slice().reverse())t.add(e[1].id);TegakiLayers.setActiveLayer(this.aLayerId),TegakiLayers.moveLayers(t,this.belowPos)},TegakiHistoryActions.AddLayer.prototype.undo=function(){TegakiLayers.deleteLayers([this.layer.id]),TegakiLayers.setActiveLayer(this.aLayerIdBefore),Tegaki.layerCounter--},TegakiHistoryActions.AddLayer.prototype.redo=function(){TegakiLayers.setActiveLayer(this.aLayerIdBefore),TegakiLayers.addLayer(this.layer),TegakiLayers.setActiveLayer(this.aLayerIdAfter)},TegakiHistoryActions.SetLayersAlpha.prototype.undo=function(){var e,t,a;for(t of this.layerAlphas)[e,t]=t,(a=TegakiLayers.getLayerById(e))&&TegakiLayers.setLayerAlpha(a,t);TegakiUI.updateLayerAlphaOpt()},TegakiHistoryActions.SetLayersAlpha.prototype.redo=function(){var e,t,a;for(t of this.layerAlphas)[e,t]=t,(a=TegakiLayers.getLayerById(e))&&TegakiLayers.setLayerAlpha(a,this.newAlpha);TegakiUI.updateLayerAlphaOpt()},TegakiHistoryActions.SetLayersAlpha.prototype.coalesce=function(e){var t;if(this.layerAlphas.length!==e.layerAlphas.length)return!1;for(t=0;t=0&&!TegakiLayers.selectedLayersHas(a[e].id);e--);for(e-=1;e>=0&&TegakiLayers.selectedLayersHas(a[e].id);e--);return(t=a[e])?t.id:0},getSelectedEdgeLayerPos:function(e){var t,a=Tegaki.layers;if(e)for(t=Tegaki.layers.length-1;t>=0&&!TegakiLayers.selectedLayersHas(a[t].id);t--);else{for(t=0;t=a.length&&(t=-1)}return t},getTopLayer:function(){return Tegaki.layers[Tegaki.layers.length-1]},getTopLayerId:function(){var e=TegakiLayers.getTopLayer();return e?e.id:0},getLayerBelowId:function(e){var t;return(t=TegakiLayers.getLayerPosById(e))<1?null:Tegaki.layers[t-1]},getLayerById:function(e){return Tegaki.layers[TegakiLayers.getLayerPosById(e)]},isSameLayerOrder:function(e,t){var a,i;if(e.length!==t.length)return!1;for(a=0;i=e[a];++a)if(i.id!==t[a].id)return!1;return!0},addLayer:function(e={}){var t,a,i,s,r,n,o,l,g;Tegaki.activeLayer?(l=Tegaki.activeLayer.id,o=TegakiLayers.getLayerPosById(Tegaki.activeLayer.id),n=$T.cls("tegaki-layer",Tegaki.layersCnt)[o]):(o=-1,n=null),n||(n=Tegaki.layersCnt.firstElementChild),(a=$T.el("canvas")).className="tegaki-layer",a.width=Tegaki.baseWidth,a.height=Tegaki.baseHeight,t=++Tegaki.layerCounter,a.id="tegaki-canvas-"+t,a.setAttribute("data-id",t),s={name:TegakiStrings.layer+" "+t,visible:!0,alpha:1},r={id:t,canvas:a,ctx:g=a.getContext("2d"),imageData:g.getImageData(0,0,a.width,a.height)};for(i in s)e[i]!==undefined&&(s[i]=e[i]),r[i]=s[i];return Tegaki.layers.splice(o+1,0,r),TegakiUI.updateLayersGridAdd(r,l),Tegaki.layersCnt.insertBefore(a,n.nextElementSibling),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.AddLayer(r,l,t)},deleteLayers:function(e,t){var a,i,s,r,n,o;o={aLayerIdBefore:Tegaki.activeLayer?Tegaki.activeLayer.id:-1,aLayerIdAfter:TegakiLayers.getTopFencedLayerId()},r=[],n=[];for(a of e)i=TegakiLayers.getLayerPosById(a),s=Tegaki.layers[i],r.push([i,s]),Tegaki.layersCnt.removeChild(s.canvas),n.push(i),TegakiUI.updateLayersGridRemove(a),TegakiUI.deleteLayerPreviewCtx(s);n=n.sort($T.sortDescCb);for(i of n)Tegaki.layers.splice(i,1);return t&&Object.assign(o,t),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.DeleteLayers(r,o)},mergeLayers:function(e){var t,a,i,s,r,n,o,l,g,d;l=[];for(o of Tegaki.layers)e.has(o.id)&&l.push(o);if(!(l.length<1)){if(1===l.length){if(!(r=TegakiLayers.getLayerBelowId(l[0].id)))return;l.unshift(r),d=!0}else r=l[l.length-1],d=!1;(t=$T.el("canvas")).width=Tegaki.baseWidth,t.height=Tegaki.baseHeight,a=t.getContext("2d"),s=$T.copyImageData(r.imageData),g=[];for(o of l)o.id!==r.id&&g.push(o.id),a.globalAlpha=o.alpha,a.drawImage(o.canvas,0,0);return $T.clearCtx(r.ctx),r.ctx.drawImage(t,0,0),TegakiLayers.syncLayerImageData(r),i=$T.copyImageData(r.imageData),n=TegakiLayers.deleteLayers(g,{tgtLayerId:r.id,tgtLayerAlpha:r.alpha,aLayerIdAfter:r.id,imageDataBefore:s,imageDataAfter:i,mergeDown:d}),TegakiLayers.setLayerAlpha(r,1),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayerPreview(r),Tegaki.onLayerStackChanged(),n}},moveLayers:function(e,t){var a,i,s,r,n,o,l;if(e.size&&Tegaki.layers.length){o=t>=Tegaki.layers.length?TegakiLayers.getTopLayer().canvas.nextElementSibling:(i=Tegaki.layers[t]).canvas,s=[],r=[],n=[],l=t,a=0;for(i of Tegaki.layers)e.has(i.id)?(t>0&&a<=t&&l--,s.push([a,i]),n.push(i)):r.push(i),++a;if(r.splice(l,0,...n),!TegakiLayers.isSameLayerOrder(r,Tegaki.layers)){Tegaki.layers=r;for(i of s)Tegaki.layersCnt.insertBefore(i[1].canvas,o);return TegakiUI.updayeLayersGridOrder(),Tegaki.onLayerStackChanged(),new TegakiHistoryActions.MoveLayers(s,t,Tegaki.activeLayer?Tegaki.activeLayer.id:-1)}}},setLayerVisibility:function(e,t){e.visible=t,t?e.canvas.classList.remove("tegaki-hidden"):e.canvas.classList.add("tegaki-hidden"),Tegaki.onLayerStackChanged(),TegakiUI.updateLayersGridVisibility(e.id,t)},setLayerAlpha:function(e,t){e.alpha=t,e.canvas.style.opacity=t},setActiveLayer:function(e){var t,a;e||(e=TegakiLayers.getTopLayerId())?(t=TegakiLayers.getLayerPosById(e))<0||(a=Tegaki.layers[t],Tegaki.activeLayer&&Tegaki.copyContextState(Tegaki.activeLayer.ctx,a.ctx),Tegaki.activeLayer=a,TegakiLayers.selectedLayersClear(),TegakiLayers.selectedLayersAdd(e),TegakiUI.updateLayersGridActive(e),TegakiUI.updateLayerAlphaOpt(),Tegaki.onLayerStackChanged()):Tegaki.activeLayer=null},syncLayerImageData(e,t=null){e.imageData=t?$T.copyImageData(t):e.ctx.getImageData(0,0,Tegaki.baseWidth,Tegaki.baseHeight)},selectedLayersHas:function(e){return Tegaki.selectedLayers.has(+e)},selectedLayersClear:function(){Tegaki.selectedLayers.clear(),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedClear()},selectedLayersAdd:function(e){Tegaki.selectedLayers.add(+e),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedSet(e,!0)},selectedLayersRemove:function(e){Tegaki.selectedLayers["delete"](+e),TegakiUI.updateLayerAlphaOpt(),TegakiUI.updateLayersGridSelectedSet(e,!1)},selectedLayersToggle:function(e){TegakiLayers.selectedLayersHas(e)?TegakiLayers.selectedLayersRemove(e):TegakiLayers.selectedLayersAdd(e)}},Tegaki={VERSION:"0.9.4",startTimeStamp:0,bg:null,canvas:null,ctx:null,layers:[],layersCnt:null,canvasCnt:null,ghostBuffer:null,blendBuffer:null,ghostBuffer32:null,blendBuffer32:null,activeLayer:null,layerCounter:0,selectedLayers:new Set,activePointerId:0,activePointerIsPen:!1,isPainting:!1,offsetX:0,offsetY:0,zoomLevel:0,zoomFactor:1,zoomFactorList:[.5,1,2,4,8,16],zoomBaseLevel:1,hasCustomCanvas:!1,TWOPI:2*Math.PI,toolList:[TegakiPencil,TegakiPen,TegakiAirbrush,TegakiBucket,TegakiTone,TegakiPipette,TegakiBlur,TegakiEraser],tools:{},tool:null,colorPaletteId:0,toolColor:"#000000",defaultTool:"pencil",bgColor:"#ffffff",maxSize:64,maxLayers:25,baseWidth:0,baseHeight:0,replayRecorder:null,replayViewer:null,onDoneCb:null,onCancelCb:null,replayMode:!1,saveReplay:!1,open:function(e={}){var t=Tegaki;if(t.bg){if(t.replayMode===!!e.replayMode)return void t.resume();t.destroy()}t.startTimeStamp=Date.now(),e.bgColor&&(t.bgColor=e.bgColor),t.hasCustomCanvas=!1,t.saveReplay=!!e.saveReplay,t.replayMode=!!e.replayMode,t.onDoneCb=e.onDone,t.onCancelCb=e.onCancel,t.baseWidth=e.width||0,t.baseHeight=e.height||0,t.createTools(),t.initKeybinds(),[t.bg,t.canvasCnt,t.layersCnt]=TegakiUI.buildUI(),document.body.appendChild(t.bg),document.body.classList.add("tegaki-backdrop"),t.replayMode?(TegakiUI.setReplayMode(!0),t.replayViewer=new TegakiReplayViewer,e.replayURL&&t.loadReplayFromURL(e.replayURL)):(t.init(),t.setTool(t.defaultTool),t.saveReplay&&(t.replayRecorder=new TegakiReplayRecorder,t.replayRecorder.start()))},init:function(){var e=Tegaki;e.createCanvas(),e.updateLayersCntSize(),e.createBuffers(),e.updatePosOffset(),e.resetLayers(),TegakiCursor.init(e.baseWidth,e.baseHeight),e.bindGlobalEvents(),TegakiUI.updateUndoRedo(0,0),TegakiUI.updateZoomLevel()},initFromReplay:function(){var e,t;t=(e=Tegaki).replayViewer,e.initToolsFromReplay(),e.baseWidth=t.canvasWidth,e.baseHeight=t.canvasHeight, +e.bgColor=$T.RgbToHex(...t.bgColor),e.toolColor=$T.RgbToHex(...t.toolColor)},initToolsFromReplay:function(){var e,t,a,i,s,r,n;t=(e=Tegaki).replayViewer;for(a in e.tools){(i=e.tools[a]).id===t.toolId&&(e.defaultTool=a),s=t.toolMap[i.id],n=["step","size","alpha","flow","tipId"];for(r of n)s[r]!==undefined&&(i[r]=s[r]);n=["sizeDynamicsEnabled","alphaDynamicsEnabled","flowDynamicsEnabled","usePreserveAlpha"];for(r of n)s[r]!==undefined&&(i[r]=!!s[r])}},resetLayers:function(){var e,t;if(Tegaki.layers.length){for(e=0,t=Tegaki.layers.length;e=TegakiColorPalettes.length||(Tegaki.colorPaletteId=e,TegakiUI.updateColorPalette())},setToolSizeUp:function(){Tegaki.setToolSize(Tegaki.tool.size+1)},setToolSizeDown:function(){Tegaki.setToolSize(Tegaki.tool.size-1)},setToolSize:function(e){e>0&&e<=Tegaki.maxSize&&(Tegaki.tool.setSize(e),Tegaki.updateCursorStatus(),Tegaki.recordEvent(TegakiEventSetToolSize,performance.now(),e),TegakiUI.updateToolSize())},setToolAlpha:function(e){(e=Math.fround(e))>=0&&e<=1&&(Tegaki.tool.setAlpha(e),Tegaki.recordEvent(TegakiEventSetToolAlpha,performance.now(),e),TegakiUI.updateToolAlpha())},setToolFlow:function(e){(e=Math.fround(e))>=0&&e<=1&&(Tegaki.tool.setFlow(e),Tegaki.recordEvent(TegakiEventSetToolFlow,performance.now(),e),TegakiUI.updateToolFlow())},setToolColor:function(e){Tegaki.toolColor=e,$T.id("tegaki-color").style.backgroundColor=e,$T.id("tegaki-colorpicker").value=e,Tegaki.tool.setColor(e),Tegaki.recordEvent(TegakiEventSetColor,performance.now(),Tegaki.tool.rgb)},setToolColorRGB:function(e,t,a){Tegaki.setToolColor($T.RgbToHex(e,t,a))},setTool:function(e){Tegaki.tools[e].set()},setToolById:function(e){var t;for(t in Tegaki.tools)if(Tegaki.tools[t].id===e)return void Tegaki.setTool(t)},setZoom:function(e){var t;(t=e+Tegaki.zoomBaseLevel)>=Tegaki.zoomFactorList.length||t<0||!Tegaki.canvas||(Tegaki.zoomLevel=e,Tegaki.zoomFactor=Tegaki.zoomFactorList[t],TegakiUI.updateZoomLevel(),Tegaki.layersCnt.style.width=Math.ceil(Tegaki.baseWidth*Tegaki.zoomFactor)+"px",Tegaki.layersCnt.style.height=Math.ceil(Tegaki.baseHeight*Tegaki.zoomFactor)+"px",Tegaki.updateCursorStatus(),Tegaki.updatePosOffset())},onZoomChange:function(){this.hasAttribute("data-in")?Tegaki.setZoom(Tegaki.zoomLevel+1):Tegaki.setZoom(Tegaki.zoomLevel-1),TegakiCursor.updateCanvasSize()},onNewClick:function(){var e,t,a,i=Tegaki;(e=prompt(TegakiStrings.promptWidth,i.canvas.width))&&(t=prompt(TegakiStrings.promptHeight,i.canvas.height))&&(t=+t,(e=+e)<1||t<1?TegakiUI.printMsg(TegakiStrings.badDimensions):(a={},i.copyContextState(i.activeLayer.ctx,a),i.resizeCanvas(e,t),i.copyContextState(a,i.activeLayer.ctx),i.setZoom(0),TegakiHistory.clear(),TegakiUI.updateLayerPreviewSize(),i.startTimeStamp=Date.now(),i.saveReplay&&(i.createTools(),i.setTool(i.defaultTool),i.replayRecorder=new TegakiReplayRecorder,i.replayRecorder.start())))},onOpenClick:function(){(!(TegakiHistory.undoStack[0]||TegakiHistory.redoStack[0])&&!Tegaki.saveReplay||confirm(TegakiStrings.confirmChangeCanvas))&&$T.id("tegaki-filepicker").click()},loadReplayFromFile:function(){Tegaki.replayViewer.debugLoadLocal()},loadReplayFromURL:function(e){TegakiUI.printMsg(TegakiStrings.loadingReplay,0),Tegaki.replayViewer.loadFromURL(e)},onExportClick:function(){Tegaki.flatten().toBlob(function(e){var t=$T.el("a");t.className="tegaki-hidden",t.download=$T.generateFilename()+".png",t.href=URL.createObjectURL(e),Tegaki.bg.appendChild(t),t.click(),Tegaki.bg.removeChild(t)},"image/png")},onUndoClick:function(){TegakiHistory.undo()},onRedoClick:function(){TegakiHistory.redo()},onHistoryChange:function(e,t,a=0){TegakiUI.updateUndoRedo(e,t),-1===a?Tegaki.recordEvent(TegakiEventUndo,performance.now()):1===a&&Tegaki.recordEvent(TegakiEventRedo,performance.now())},onDoneClick:function(){Tegaki.hide(),Tegaki.onDoneCb()},onCancelClick:function(){confirm(TegakiStrings.confirmCancel)&&(Tegaki.destroy(),Tegaki.onCancelCb())},onCloseViewerClick:function(){Tegaki.replayViewer.destroy(),Tegaki.destroy()},onToolSizeChange:function(){var e=+this.value;e<1?e=1:e>Tegaki.maxSize&&(e=Tegaki.maxSize),Tegaki.setToolSize(e)},onToolAlphaChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setToolAlpha(e)},onToolFlowChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setToolFlow(e)},onToolPressureSizeClick:function(){Tegaki.tool.useSizeDynamics&&Tegaki.setToolSizeDynamics(!Tegaki.tool.sizeDynamicsEnabled)},setToolSizeDynamics:function(e){Tegaki.tool.setSizeDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolSizeDynamics,performance.now(),+e)},onToolPressureAlphaClick:function(){Tegaki.tool.useAlphaDynamics&&Tegaki.setToolAlphaDynamics(!Tegaki.tool.alphaDynamicsEnabled)},setToolAlphaDynamics:function(e){Tegaki.tool.setAlphaDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolAlphaDynamics,performance.now(),+e)},onToolPressureFlowClick:function(){Tegaki.tool.useFlowDynamics&&Tegaki.setToolFlowDynamics(!Tegaki.tool.flowDynamicsEnabled)},setToolFlowDynamics:function(e){Tegaki.tool.setFlowDynamics(e),TegakiUI.updateToolDynamics(),Tegaki.recordEvent(TegakiEventSetToolFlowDynamics,performance.now(),+e)},onToolPreserveAlphaClick:function(){Tegaki.tool.usePreserveAlpha&&Tegaki.setToolPreserveAlpha(!Tegaki.tool.preserveAlphaEnabled)},setToolPreserveAlpha:function(e){Tegaki.tool.setPreserveAlpha(e),TegakiUI.updateToolPreserveAlpha(),Tegaki.recordEvent(TegakiEventPreserveAlpha,performance.now(),+e)},onToolTipClick:function(e){var t=+e.target.getAttribute("data-id");t!==Tegaki.tool.tipId&&Tegaki.setToolTip(t)},setToolTip:function(e){Tegaki.tool.setTip(e),TegakiUI.updateToolShape(),Tegaki.recordEvent(TegakiEventSetToolTip,performance.now(),e)},onLayerSelectorClick:function(e){var t=+this.getAttribute("data-id");t&&!e.target.classList.contains("tegaki-ui-cb")&&(e.ctrlKey?Tegaki.toggleSelectedLayer(t):Tegaki.setActiveLayer(t))},toggleSelectedLayer:function(e){TegakiLayers.selectedLayersToggle(e),Tegaki.recordEvent(TegakiEventToggleLayerSelection,performance.now(),e)},setActiveLayer:function(e){TegakiLayers.setActiveLayer(e),Tegaki.recordEvent(TegakiEventSetActiveLayer,performance.now(),e)},onLayerAlphaDragStart:function(e){TegakiUI.setupDragLabel(e,Tegaki.onLayerAlphaDragMove)},onLayerAlphaDragMove:function(e){var t;e&&((t=Tegaki.activeLayer.alpha+e/100)<0?t=0:t>1&&(t=1),Tegaki.setSelectedLayersAlpha(t))},onLayerAlphaChange:function(){var e=+this.value;(e/=100)<0?e=0:e>1&&(e=1),Tegaki.setSelectedLayersAlpha(e)},setSelectedLayersAlpha:function(e){var t,a,i;if((e=Math.fround(e))>=0&&e<=1&&Tegaki.selectedLayers.size>0){i=[];for(a of Tegaki.selectedLayers)(t=TegakiLayers.getLayerById(a))&&(i.push([t.id,t.alpha]),TegakiLayers.setLayerAlpha(t,e));TegakiUI.updateLayerAlphaOpt(),TegakiHistory.push(new TegakiHistoryActions.SetLayersAlpha(i,e)),Tegaki.recordEvent(TegakiEventSetSelectedLayersAlpha,performance.now(),e)}},onLayerNameChangeClick:function(){var e,t,a;e=+this.getAttribute("data-id"),(a=TegakiLayers.getLayerById(e))&&(t=prompt(undefined,a.name))&&Tegaki.setLayerName(e,t)},setLayerName:function(e,t){var a,i;t=t.trim().slice(0,25),(i=TegakiLayers.getLayerById(e))&&t&&t!==i.name&&(a=i.name,i.name=t,TegakiUI.updateLayerName(i),TegakiHistory.push(new TegakiHistoryActions.SetLayerName(e,a,t)),Tegaki.recordEvent(TegakiEventHistoryDummy,performance.now()))},onLayerAddClick:function(){Tegaki.addLayer()},addLayer:function(){var e;Tegaki.layers.length>=Tegaki.maxLayers?TegakiUI.printMsg(TegakiStrings.tooManyLayers):(TegakiHistory.push(e=TegakiLayers.addLayer()),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventAddLayer,performance.now()))},onLayerDeleteClick:function(){Tegaki.deleteSelectedLayers()},deleteSelectedLayers:function(){var e,t;(t=Tegaki.selectedLayers).size!==Tegaki.layers.length&&(!t.size||Tegaki.layers.length<2||(TegakiHistory.push(e=TegakiLayers.deleteLayers(t)),TegakiLayers.selectedLayersClear(),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventDeleteLayers,performance.now())))},onLayerToggleVisibilityClick:function(){Tegaki.toggleLayerVisibility(+this.getAttribute("data-id"))},toggleLayerVisibility:function(e){var t=TegakiLayers.getLayerById(e);TegakiLayers.setLayerVisibility(t,!t.visible),Tegaki.recordEvent(TegakiEventToggleLayerVisibility,performance.now(),e)},onMergeLayersClick:function(){Tegaki.mergeSelectedLayers()},mergeSelectedLayers:function(){var e;Tegaki.selectedLayers.size&&(e=TegakiLayers.mergeLayers(Tegaki.selectedLayers))&&(TegakiHistory.push(e),TegakiLayers.setActiveLayer(e.aLayerIdAfter),Tegaki.recordEvent(TegakiEventMergeLayers,performance.now()))},onMoveLayerClick:function(e){var t,a;Tegaki.selectedLayers.size&&(a=e.target.hasAttribute("data-up"),(t=TegakiLayers.getSelectedEdgeLayerPos(a))<0||(a?t+=2:t>=1&&t--,Tegaki.moveSelectedLayers(t)))},moveSelectedLayers:function(e){TegakiHistory.push(TegakiLayers.moveLayers(Tegaki.selectedLayers,e)),Tegaki.recordEvent(TegakiEventMoveLayers,performance.now(),e)},onToolClick:function(){Tegaki.setTool(this.getAttribute("data-tool"))},onToolChanged:function(e){var t;Tegaki.tool=e,(t=$T.cls("tegaki-tool-active")[0])&&t.classList.remove("tegaki-tool-active"),$T.id("tegaki-tool-btn-"+e.name).classList.add("tegaki-tool-active"),Tegaki.recordEvent(TegakiEventSetTool,performance.now(),Tegaki.tool.id),TegakiUI.onToolChanged(),Tegaki.updateCursorStatus()},onLayerStackChanged:function(){},onOpenFileSelected:function(){var e;this.files&&this.files[0]&&((e=new Image).onload=Tegaki.onOpenImageLoaded,e.onerror=Tegaki.onOpenImageError,e.src=URL.createObjectURL(this.files[0]))},onOpenImageLoaded:function(){var e={},t=Tegaki;t.hasCustomCanvas=!0,t.copyContextState(t.activeLayer.ctx,e),t.resizeCanvas(this.naturalWidth,this.naturalHeight),t.activeLayer.ctx.drawImage(this,0,0),TegakiLayers.syncLayerImageData(t.activeLayer),t.copyContextState(e,t.activeLayer.ctx),t.setZoom(0),TegakiHistory.clear(),TegakiUI.updateLayerPreviewSize(!0),t.startTimeStamp=Date.now(),t.saveReplay&&(t.replayRecorder.stop(),t.replayRecorder=null,t.saveReplay=!1,TegakiUI.setRecordingStatus(!1))},onOpenImageError:function(){TegakiUI.printMsg(TegakiStrings.errorLoadImage)},resizeCanvas:function(e,t){Tegaki.baseWidth=e,Tegaki.baseHeight=t,Tegaki.createBuffers(),Tegaki.canvas.width=e,Tegaki.canvas.height=t,Tegaki.ctx.fillStyle=Tegaki.bgColor,Tegaki.ctx.fillRect(0,0,e,t),Tegaki.activeLayer=null,Tegaki.resetLayers(),Tegaki.updateLayersCntSize(),Tegaki.updatePosOffset(),TegakiCursor.updateCanvasSize()},copyContextState:function(e,t){var a,i,s=["lineCap","lineJoin","strokeStyle","fillStyle","globalAlpha","lineWidth","globalCompositeOperation"];for(a=0;i=s[a];++a)t[i]=e[i]},updateCursorStatus:function(){!Tegaki.tool.noCursor&&Tegaki.tool.size>1?Tegaki.cursor=TegakiCursor.generate(Tegaki.tool.size):(Tegaki.cursor=!1,TegakiCursor.clear())},updatePosOffset:function(){var e=Tegaki.canvas.getBoundingClientRect();Tegaki.offsetX=e.left+window.pageXOffset+Tegaki.canvasCnt.scrollLeft+Tegaki.layersCnt.scrollLeft,Tegaki.offsetY=e.top+window.pageYOffset+Tegaki.canvasCnt.scrollTop+Tegaki.layersCnt.scrollTop},getScrollbarSizes:function(){return[Tegaki.canvasCnt.offsetWidth-Tegaki.canvasCnt.clientWidth,Tegaki.canvasCnt.offsetHeight-Tegaki.canvasCnt.clientHeight]},isScrollbarClick:function(e){var[t,a]=Tegaki.getScrollbarSizes();return t>0&&e.clientX>=Tegaki.canvasCnt.offsetLeft+Tegaki.canvasCnt.clientWidth&&e.clientX<=Tegaki.canvasCnt.offsetLeft+Tegaki.canvasCnt.clientWidth+t||a>0&&e.clientY>=Tegaki.canvasCnt.offsetTop+Tegaki.canvasCnt.clientHeight&&e.clientY<=Tegaki.canvasCnt.offsetTop+Tegaki.canvasCnt.clientHeight+t},onPointerMove:function(e){var t,a,i,s,r,n;if(Tegaki.cursor&&TegakiCursor.render(e.clientX,e.clientY),e.mozInputSource!==undefined){if(Tegaki.activePointerIsPen&&"mouse"===e.pointerType)return}else if(Tegaki.activePointerId!==e.pointerId)return void(Tegaki.activePointerId=e.pointerId);if(Tegaki.isPainting)if(s=Tegaki.tool,Tegaki.activePointerIsPen&&e.getCoalescedEvents){t=e.getCoalescedEvents(),r=e.timeStamp;for(e of t)a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1),s.enabledDynamics()?(n=TegakiPressure.toShort(e.pressure),TegakiPressure.push(n),Tegaki.recordEvent(TegakiEventDraw,r,a,i,n)):Tegaki.recordEvent(TegakiEventDrawNoP,r,a,i),s.draw(a,i)}else a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1),n=TegakiPressure.toShort(e.pressure),Tegaki.recordEvent(TegakiEventDraw,e.timeStamp,a,i,n),TegakiPressure.push(n),s.draw(a,i);else a=Tegaki.getPointerPos(e,0),i=Tegaki.getPointerPos(e,1)},onPointerDown:function(e){var t,a,i,s;Tegaki.cursor&&TegakiCursor.render(e.clientX,e.clientY),Tegaki.isScrollbarClick(e)||(Tegaki.activePointerId=e.pointerId,Tegaki.activePointerIsPen="pen"===e.pointerType,null!==Tegaki.activeLayer?TegakiLayers.getActiveLayer().visible?(t=Tegaki.getPointerPos(e,0),a=Tegaki.getPointerPos(e,1),2===e.button||e.altKey?(e.preventDefault(),Tegaki.isPainting=!1,Tegaki.tools.pipette.draw(t,a)):0===e.button&&(e.preventDefault(),(i=Tegaki.tool).enabledDynamics()?(s=TegakiPressure.toShort(e.pressure),TegakiPressure.push(s),Tegaki.recordEvent(TegakiEventDrawStart,e.timeStamp,t,a,s)):Tegaki.recordEvent(TegakiEventDrawStartNoP,e.timeStamp,t,a),Tegaki.isPainting=!0,TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),i.start(t,a))):e.target.parentNode===Tegaki.layersCnt&&TegakiUI.printMsg(TegakiStrings.hiddenActiveLayer):e.target.parentNode===Tegaki.layersCnt&&TegakiUI.printMsg(TegakiStrings.noActiveLayer))},onPointerUp:function(e){Tegaki.activePointerId=e.pointerId,Tegaki.activePointerIsPen=!1,Tegaki.isPainting&&(Tegaki.recordEvent(TegakiEventDrawCommit,e.timeStamp),Tegaki.tool.commit(),TegakiUI.updateLayerPreview(Tegaki.activeLayer),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,1),TegakiHistory.push(TegakiHistory.pendingAction),Tegaki.isPainting=!1)},onPointerOut:function(){TegakiCursor.clearAll()},onDummy:function(e){e.preventDefault(),e.stopPropagation()},recordEvent(e,...t){Tegaki.replayRecorder&&Tegaki.replayRecorder.push(new e(...t))}},TegakiColorPalettes=[["#ffffff","#000000","#888888","#b47575","#c096c0","#fa9696","#8080ff","#ffb6ff","#e7e58d","#25c7c9","#99cb7b","#e7962d","#f9ddcf","#fcece2"],["#000000","#ffffff","#7f7f7f","#c3c3c3","#880015","#b97a57","#ed1c24","#ffaec9","#ff7f27","#ffc90e","#fff200","#efe4b0","#22b14c","#b5e61d","#00a2e8","#99d9ea","#3f48cc","#7092be","#a349a4","#c8bfe7"],["#000000","#ffffff","#8a8a8a","#cacaca","#fcece2","#f9ddcf","#e0a899","#a05b53","#7a444a","#960018","#c41e3a","#de4537","#ff3300","#ff9800","#ffc107","#ffd700","#ffeb3b","#ffffcc","#f3e5ab","#cddc39","#8bc34a","#4caf50","#3e8948","#355e3b","#3eb489","#f0f8ff","#87ceeb","#6699cc","#007fff","#2d68c4","#364478","#352c4a","#9c27b0","#da70d6","#ff0090","#fa8072","#f19cbb","#c78b95"]],TegakiPressure={pressureNow:0,pressureThen:0,toShort:function(e){return 0|65535*e},get:function(){return this.pressureNow},lerp:function(e){return this.pressureThen*(1-e)+this.pressureNow*e},push:function(e){this.pressureThen=this.pressureNow,this.pressureNow=e/65535},set:function(e){this.pressureThen=this.pressureNow=e/65535}};class TegakiEvent_void{constructor(){this.size=5}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp)}static unpack(e){return new this(e.readUint32())}}class TegakiEvent_c{constructor(){this.size=6}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeUint8(this.value)}static unpack(e){return new this(e.readUint32(),e.readUint8())}}class TegakiEventPrelude extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){}}class TegakiEventConclusion extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){}}class TegakiEventHistoryDummy extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.push(new TegakiHistoryActions.Dummy)}}class TegakiEventDrawStart{constructor(e,t,a,i){this.timeStamp=e,this.x=t,this.y=a,this.pressure=i,this.type=TegakiEvents[this.constructor.name][0],this.size=11}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y),e.writeUint16(this.pressure)}static unpack(e){var t,a,i,s;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),s=e.readUint16(),new TegakiEventDrawStart(t,a,i,s)}dispatch(){TegakiPressure.set(this.pressure),TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),Tegaki.tool.start(this.x,this.y)}}class TegakiEventDrawStartNoP{constructor(e,t,a){this.timeStamp=e,this.x=t,this.y=a,this.type=TegakiEvents[this.constructor.name][0],this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y)}static unpack(e){var t,a,i;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),new TegakiEventDrawStartNoP(t,a,i)}dispatch(){TegakiPressure.set(.5),TegakiHistory.pendingAction=new TegakiHistoryActions.Draw(Tegaki.activeLayer.id),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,0),Tegaki.tool.start(this.x,this.y)}}class TegakiEventDraw{constructor(e,t,a,i){this.timeStamp=e,this.x=t,this.y=a,this.pressure=i,this.type=TegakiEvents[this.constructor.name][0],this.size=11}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y),e.writeUint16(this.pressure)}static unpack(e){var t,a,i,s;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),s=e.readUint16(),new TegakiEventDraw(t,a,i,s)}dispatch(){TegakiPressure.push(this.pressure),Tegaki.tool.draw(this.x,this.y)}}class TegakiEventDrawNoP{constructor(e,t,a){this.timeStamp=e,this.x=t,this.y=a,this.type=TegakiEvents[this.constructor.name][0],this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeInt16(this.x),e.writeInt16(this.y)}static unpack(e){var t,a,i;return t=e.readUint32(),a=e.readInt16(),i=e.readInt16(),new TegakiEventDrawNoP(t,a,i)}dispatch(){TegakiPressure.push(.5),Tegaki.tool.draw(this.x,this.y)}}class TegakiEventDrawCommit extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.tool.commit(),TegakiUI.updateLayerPreview(Tegaki.activeLayer),TegakiHistory.pendingAction.addCanvasState(Tegaki.activeLayer.imageData,1),TegakiHistory.push(TegakiHistory.pendingAction),Tegaki.isPainting=!1}}class TegakiEventUndo extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.undo()}}class TegakiEventRedo extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){TegakiHistory.redo()}}class TegakiEventSetColor{constructor(e,t){this.timeStamp=e,[this.r,this.g,this.b]=t,this.type=TegakiEvents[this.constructor.name][0],this.size=8,this.coalesce=!0}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeUint8(this.r),e.writeUint8(this.g),e.writeUint8(this.b)}static unpack(e){var t,a;return t=e.readUint32(),a=[e.readUint8(),e.readUint8(),e.readUint8()],new TegakiEventSetColor(t,a)}dispatch(){Tegaki.setToolColorRGB(this.r,this.g,this.b)}}class TegakiEventSetTool extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolById(this.value)}}class TegakiEventSetToolSize extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolSize(this.value)}}class TegakiEventSetToolAlpha{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setToolAlpha(this.value)}}class TegakiEventSetToolFlow{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setToolFlow(this.value)}}class TegakiEventPreserveAlpha extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolPreserveAlpha(!!this.value)}}class TegakiEventSetToolSizeDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolSizeDynamics(!!this.value)}}class TegakiEventSetToolAlphaDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolAlphaDynamics(!!this.value)}}class TegakiEventSetToolFlowDynamics extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolFlowDynamics(!!this.value)}}class TegakiEventSetToolTip extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0}dispatch(){Tegaki.setToolTip(this.value)}}class TegakiEventAddLayer extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.addLayer()}}class TegakiEventDeleteLayers extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.deleteSelectedLayers()}}class TegakiEventMoveLayers extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.moveSelectedLayers(this.value)}}class TegakiEventMergeLayers extends TegakiEvent_void{constructor(e){super(),this.timeStamp=e,this.type=TegakiEvents[this.constructor.name][0]}static unpack(e){return super.unpack(e)}dispatch(){Tegaki.mergeSelectedLayers()}}class TegakiEventToggleLayerVisibility extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.toggleLayerVisibility(this.value)}}class TegakiEventSetActiveLayer extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.setActiveLayer(this.value)}}class TegakiEventToggleLayerSelection extends TegakiEvent_c{constructor(e,t){super(),this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0]}dispatch(){Tegaki.toggleSelectedLayer(this.value)}}class TegakiEventSetSelectedLayersAlpha{constructor(e,t){this.timeStamp=e,this.value=t,this.type=TegakiEvents[this.constructor.name][0],this.coalesce=!0,this.size=9}pack(e){e.writeUint8(this.type),e.writeUint32(this.timeStamp),e.writeFloat32(this.value)}static unpack(e){return new this(e.readUint32(),e.readFloat32())}dispatch(){Tegaki.setSelectedLayersAlpha(this.value)}}const TegakiEvents=Object.freeze({TegakiEventPrelude:[0,TegakiEventPrelude],TegakiEventDrawStart:[1,TegakiEventDrawStart],TegakiEventDraw:[2,TegakiEventDraw],TegakiEventDrawCommit:[3,TegakiEventDrawCommit],TegakiEventUndo:[4,TegakiEventUndo],TegakiEventRedo:[5,TegakiEventRedo],TegakiEventSetColor:[6,TegakiEventSetColor],TegakiEventDrawStartNoP:[7,TegakiEventDrawStartNoP],TegakiEventDrawNoP:[8,TegakiEventDrawNoP],TegakiEventSetTool:[10,TegakiEventSetTool],TegakiEventSetToolSize:[11,TegakiEventSetToolSize],TegakiEventSetToolAlpha:[12,TegakiEventSetToolAlpha],TegakiEventSetToolSizeDynamics:[13,TegakiEventSetToolSizeDynamics],TegakiEventSetToolAlphaDynamics:[14,TegakiEventSetToolAlphaDynamics],TegakiEventSetToolTip:[15,TegakiEventSetToolTip],TegakiEventPreserveAlpha:[16,TegakiEventPreserveAlpha],TegakiEventSetToolFlowDynamics:[17,TegakiEventSetToolFlowDynamics],TegakiEventSetToolFlow:[18,TegakiEventSetToolFlow],TegakiEventAddLayer:[20,TegakiEventAddLayer],TegakiEventDeleteLayers:[21,TegakiEventDeleteLayers],TegakiEventMoveLayers:[22,TegakiEventMoveLayers],TegakiEventMergeLayers:[23,TegakiEventMergeLayers],TegakiEventToggleLayerVisibility:[24,TegakiEventToggleLayerVisibility],TegakiEventSetActiveLayer:[25,TegakiEventSetActiveLayer],TegakiEventToggleLayerSelection:[26,TegakiEventToggleLayerSelection],TegakiEventSetSelectedLayersAlpha:[27,TegakiEventSetSelectedLayersAlpha],TegakiEventHistoryDummy:[254,TegakiEventHistoryDummy],TegakiEventConclusion:[255,TegakiEventConclusion]});class TegakiReplayRecorder{constructor(){this.formatVersion=1,this.compressed=!0,this.tegakiVersion=Tegaki.VERSION.split(".").map(e=>+e),this.canvasWidth=Tegaki.baseWidth,this.canvasHeight=Tegaki.baseHeight,this.bgColor=$T.hexToRgb(Tegaki.bgColor),this.toolColor=$T.hexToRgb(Tegaki.toolColor),this.toolId=Tegaki.tools[Tegaki.defaultTool].id,this.toolList=this.buildToolList(Tegaki.tools),this.startTimeStamp=0,this.endTimeStamp=0,this.recording=!1,this.events=[],this.mimeType="application/octet-stream"}buildToolList(e){var t,a,i;i=[];for(t in e)a=e[t],i.push({id:a.id,size:a.size,alpha:a.alpha,flow:a.flow,step:a.step,sizeDynamicsEnabled:+a.sizeDynamicsEnabled,alphaDynamicsEnabled:+a.alphaDynamicsEnabled,flowDynamicsEnabled:+a.flowDynamicsEnabled,usePreserveAlpha:+a.usePreserveAlpha,tipId:a.tipId});return i}start(){this.recording||(this.endTimeStamp>0&&(this.events.pop(),this.endTimeStamp=0),0===this.startTimeStamp&&(this.events.push(new TegakiEventPrelude(performance.now())),this.startTimeStamp=Date.now()),this.recording=!0)}stop(){ +0!==this.startTimeStamp&&this.recording&&(this.events.push(new TegakiEventConclusion(performance.now())),this.endTimeStamp=Date.now(),this.recording=!1)}push(e){this.recording&&(e.coalesce&&this.events[this.events.length-1].type===e.type?this.events[this.events.length-1]=e:this.events.push(e))}getEventStackSize(){var e,t;t=4;for(e of this.events)t+=e.size;return t}getHeaderSize(){return 12}getMetaSize(){return 21}getToolSize(){return 19}getToolListSize(){return 2+this.toolList.length*this.getToolSize()}writeToolList(e){var t,a,i;i=[["id","Uint8"],["size","Uint8"],["alpha","Float32"],["step","Float32"],["sizeDynamicsEnabled","Uint8"],["alphaDynamicsEnabled","Uint8"],["usePreserveAlpha","Uint8"],["tipId","Int8"],["flow","Float32"],["flowDynamicsEnabled","Uint8"]],e.writeUint8(this.toolList.length),e.writeUint8(this.getToolSize());for(t of this.toolList)for(a of i)e["write"+a[1]](t[a[0]])}writeMeta(e){e.writeUint16(this.getMetaSize()),e.writeUint32(Math.ceil(this.startTimeStamp/1e3)),e.writeUint32(Math.ceil(this.endTimeStamp/1e3)),e.writeUint16(this.canvasWidth),e.writeUint16(this.canvasHeight),e.writeUint8(this.bgColor[0]),e.writeUint8(this.bgColor[1]),e.writeUint8(this.bgColor[2]),e.writeUint8(this.toolColor[0]),e.writeUint8(this.toolColor[1]),e.writeUint8(this.toolColor[2]),e.writeUint8(this.toolId)}writeEventStack(e){var t;e.writeUint32(this.events.length);for(t of this.events)t.pack(e)}writeHeader(e,t){e.writeUint8(84),e.writeUint8(71),e.writeUint8(75),e.writeUint8(+this.compressed),e.writeUint32(t),e.writeUint8(this.tegakiVersion[0]),e.writeUint8(this.tegakiVersion[1]),e.writeUint8(this.tegakiVersion[2]),e.writeUint8(this.formatVersion)}compressData(e){return UZIP.deflateRaw(new Uint8Array(e.buf),{level:9})}toUint8Array(){var e,t,a,i,s,r;return this.startTimeStamp&&this.endTimeStamp?(e=this.getHeaderSize(),t=this.getMetaSize()+this.getToolListSize()+this.getEventStackSize(),a=new ArrayBuffer(t),i=new TegakiBinWriter(a),this.writeMeta(i),this.writeToolList(i),this.writeEventStack(i),s=this.compressData(i),i=new TegakiBinWriter(new ArrayBuffer(e+s.length)),this.writeHeader(i,t),(r=new Uint8Array(i.buf)).set(s,e),r):null}toBlob(){var e=this.toUint8Array();return e?new Blob([e.buffer],{type:this.mimeType}):null}}class TegakiReplayViewer{constructor(){this.formatVersion=1,this.compressed=!0,this.tegakiVersion=[0,0,0],this.dataSize=0,this.canvasWidth=0,this.canvasHeight=0,this.bgColor=[0,0,0],this.toolColor=[0,0,0],this.toolId=1,this.toolMap={},this.startTimeStamp=0,this.endTimeStamp=0,this.loaded=!1,this.playing=!1,this.gapless=!0,this.autoPaused=!1,this.destroyed=!1,this.speedIndex=1,this.speedList=[.5,1,2,5,10,25],this.speed=this.speedList[this.speedIndex],this.maxEventsPerFrame=50,this.maxEventCount=864e4,this.events=[],this.preludePos=0,this.currentPos=0,this.conclusionPos=0,this.duration=0,this.playTimeStart=0,this.playTimeCurrent=0,this.eventIndex=0,this.maxCanvasWH=8192,this.maxGapTime=3e3,this.uiAccTime=0,this.onFrameThis=this.onFrame.bind(this)}destroy(){this.destroyed=!0,this.pause(),this.events=null}speedUp(){this.speedIndex+1=0&&(this.speed=this.speedList[--this.speedIndex])}toggleGapless(){this.gapless=!this.gapless}getCurrentPos(){return this.currentPos}getDuration(){return this.duration}loadFromURL(e){fetch(e).then(e=>this.onResponseReady(e))["catch"](e=>this.onLoadError(e))}onResponseReady(e){e.ok?e.arrayBuffer().then(e=>this.onResponseBodyReady(e))["catch"](e=>this.onLoadError(e)):this.onLoadError(e.statusText)}onResponseBodyReady(e){this.loadFromBuffer(e),Tegaki.onReplayLoaded()}onLoadError(e){TegakiUI.printMsg(TegakiStrings.errorLoadReplay+e,0)}autoPause(){this.autoPaused=!0,this.pause()}pause(e){window.cancelAnimationFrame(this.onFrameThis),this.playing=!1,e&&(this.currentPos=0,this.eventIndex=0),Tegaki.onReplayTimeChanged(),Tegaki.onReplayPlayPauseChanged()}rewind(){this.autoPaused=!1,this.pause(!0),Tegaki.onReplayReset()}play(){this.playTimeStart=performance.now(),this.playTimeCurrent=this.playTimeStart,this.playing=!0,this.autoPaused=!1,this.uiAccTime=0,Tegaki.onReplayPlayPauseChanged(),window.requestAnimationFrame(this.onFrameThis)}togglePlayPause(){this.playing?this.pause():this.play()}onFrame(e){var t=e-this.playTimeCurrent;this.playing&&(this.playTimeCurrent=e,this.step(t),this.uiAccTime+=t,this.uiAccTime>1e3&&(Tegaki.onReplayTimeChanged(),this.uiAccTime=0),this.currentPosthis.maxGapTime&&(this.currentPos=t.timeStamp-this.preludePos,a=t.timeStamp),i=0;this.eventIndex=this.maxEventsPerFrame){this.currentPos=t.timeStamp-this.preludePos;break}t.dispatch(),++this.eventIndex,++i}}getEventIdMap(){var e,t,a;e={};for(t in TegakiEvents)e[(a=TegakiEvents[t])[0]]=a[1];return e}readToolMap(e){var t,a,i,s,r,n,o;for(this.toolMap={},n=[["id","Uint8"],["size","Uint8"],["alpha","Float32"],["step","Float32"],["sizeDynamicsEnabled","Uint8"],["alphaDynamicsEnabled","Uint8"],["usePreserveAlpha","Uint8"],["tipId","Int8"],["flow","Float32"],["flowDynamicsEnabled","Uint8"]],a=e.readUint8(),i=e.readUint8(),t=0;t=o)break;s[r[0]]=e["read"+r[1]]()}this.toolMap[s.id]=s,e.pos=o}}readHeader(e){if("TGK"!==String.fromCharCode(e.readUint8(),e.readUint8(),e.readUint8()))throw"invalid header";this.compressed=1===e.readUint8(),this.dataSize=e.readUint32(),this.tegakiVersion[0]=e.readUint8(),this.tegakiVersion[1]=e.readUint8(),this.tegakiVersion[2]=e.readUint8(),this.formatVersion=e.readUint8()}decompressData(e){return UZIP.inflateRaw(new Uint8Array(e.buf,e.pos),new Uint8Array(this.dataSize))}readMeta(e){var t,a;if(a=e.readUint16(),t=e.pos+a-2,this.startTimeStamp=1e3*e.readUint32(),this.endTimeStamp=1e3*e.readUint32(),this.canvasWidth=e.readUint16(),this.canvasHeight=e.readUint16(),this.canvasWidth>this.maxCanvasWH||this.canvasHeight>this.maxCanvasWH)throw"canvas too large";this.bgColor[0]=e.readUint8(),this.bgColor[1]=e.readUint8(),this.bgColor[2]=e.readUint8(),this.toolColor[0]=e.readUint8(),this.toolColor[1]=e.readUint8(),this.toolColor[2]=e.readUint8(),this.toolId=e.readUint8(),e.pos=t}readEventStack(e){var t,a,i,s,r;if(r=this.getEventIdMap(),(a=e.readUint32())<1||a>this.maxEventCount)throw"invalid event count";for(t=0;t0&&(TegakiUI.statusTimeout=setTimeout(TegakiUI.clearMsg,5e3))},clearMsg:function(){TegakiUI.statusTimeout&&(clearTimeout(TegakiUI.statusTimeout),TegakiUI.statusTimeout=0),$T.id("tegaki-status-output").textContent=""},buildUI:function(){var e,t,a,i,s,r;return(e=$T.el("div")).id="tegaki",(a=$T.el("div")).id="tegaki-menu-cnt",Tegaki.replayMode?(a.appendChild(TegakiUI.buildViewerMenuBar()),a.appendChild(TegakiUI.buildReplayControls())):a.appendChild(TegakiUI.buildMenuBar()),a.appendChild(TegakiUI.buildToolModeBar()),e.appendChild(a),e.appendChild(TegakiUI.buildDummyFilePicker()),(t=$T.el("div")).id="tegaki-tools-cnt",t.appendChild(TegakiUI.buildToolsMenu()),e.appendChild(t),[r,s]=TegakiUI.buildCanvasCnt(),e.appendChild(r),(i=$T.el("div")).id="tegaki-ctrl-cnt",i.appendChild(TegakiUI.buildZoomCtrlGroup()),i.appendChild(TegakiUI.buildColorCtrlGroup(Tegaki.toolColor)),i.appendChild(TegakiUI.buildSizeCtrlGroup()),i.appendChild(TegakiUI.buildAlphaCtrlGroup()),i.appendChild(TegakiUI.buildFlowCtrlGroup()),i.appendChild(TegakiUI.buildLayersCtrlGroup()),e.appendChild(i),e.appendChild(TegakiUI.buildStatusCnt()),[e,r,s]},buildDummyFilePicker:function(){var e=$T.el("input");return e.type="file",e.id="tegaki-filepicker",e.className="tegaki-hidden",e.accept="image/png, image/jpeg",$T.on(e,"change",Tegaki.onOpenFileSelected),e},buildMenuBar:function(){var e,t;return(e=$T.el("div")).id="tegaki-menu-bar",(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.newCanvas,$T.on(t,"click",Tegaki.onNewClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.open,$T.on(t,"click",Tegaki.onOpenClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings["export"],$T.on(t,"click",Tegaki.onExportClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-undo-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.undo,t.title=TegakiKeybinds.getCaption("undo"),$T.on(t,"click",Tegaki.onUndoClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-redo-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.redo,t.title=TegakiKeybinds.getCaption("redo"),$T.on(t,"click",Tegaki.onRedoClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-mb-btn",t.textContent=TegakiStrings.close,$T.on(t,"click",Tegaki.onCancelClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-finish-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.finish,$T.on(t,"click",Tegaki.onDoneClick),e.appendChild(t),e},buildViewerMenuBar:function(){var e,t;return(e=$T.el("div")).id="tegaki-menu-bar",(t=$T.el("span")).id="tegaki-finish-btn",t.className="tegaki-mb-btn",t.textContent=TegakiStrings.close,$T.on(t,"click",Tegaki.onCloseViewerClick),e.appendChild(t),e},buildToolModeBar:function(){var e,t,a,i;return(e=$T.el("div")).id="tegaki-toolmode-bar",Tegaki.tool||e.classList.add("tegaki-hidden"),(t=$T.el("span")).id="tegaki-tool-mode-dynamics",t.className="tegaki-toolmode-grp",(a=$T.el("span")).className="tegaki-toolmode-lbl",a.textContent=TegakiStrings.pressure,t.appendChild(a),(a=$T.el("span")).id="tegaki-tool-mode-dynamics-ctrl",a.className="tegaki-toolmode-ctrl",(i=$T.el("span")).id="tegaki-tool-mode-dynamics-size",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.size,$T.on(i,"mousedown",Tegaki.onToolPressureSizeClick),a.appendChild(i),(i=$T.el("span")).id="tegaki-tool-mode-dynamics-alpha",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.alpha,$T.on(i,"mousedown",Tegaki.onToolPressureAlphaClick),a.appendChild(i),(i=$T.el("span")).id="tegaki-tool-mode-dynamics-flow",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.flow,$T.on(i,"mousedown",Tegaki.onToolPressureFlowClick),a.appendChild(i),t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-tool-mode-mask",t.className="tegaki-toolmode-grp",(a=$T.el("span")).id="tegaki-toolmode-ctrl-tip",a.className="tegaki-toolmode-ctrl",(i=$T.el("span")).id="tegaki-tool-mode-mask-alpha",i.className="tegaki-sw-btn",i.textContent=TegakiStrings.preserveAlpha,$T.on(i,"mousedown",Tegaki.onToolPreserveAlphaClick),a.appendChild(i),t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-tool-mode-tip",t.className="tegaki-toolmode-grp",(a=$T.el("span")).className="tegaki-toolmode-lbl",a.textContent=TegakiStrings.tip,t.appendChild(a),(a=$T.el("span")).id="tegaki-tool-mode-tip-ctrl",a.className="tegaki-toolmode-ctrl",t.appendChild(a),e.appendChild(t),e},buildToolsMenu:function(){var e,t,a,i;(e=$T.el("div")).id="tegaki-tools-grid";for(i in Tegaki.tools)(t=$T.el("span")).setAttribute("data-tool",i),a=TegakiStrings[i],Tegaki.tools[i].keybind&&(a+=" ("+Tegaki.tools[i].keybind.toUpperCase()+")"),t.setAttribute("title",a),t.id="tegaki-tool-btn-"+i,t.className="tegaki-tool-btn tegaki-icon tegaki-"+i,$T.on(t,"click",Tegaki.onToolClick),e.appendChild(t);return e},buildCanvasCnt:function(){var e,t,a;return(e=$T.el("div")).id="tegaki-canvas-cnt",(t=$T.el("div")).id="tegaki-layers-wrap",(a=$T.el("div")).id="tegaki-layers",t.appendChild(a),e.appendChild(t),[e,a]},buildCtrlGroup:function(e,t){var a,i;return(a=$T.el("div")).className="tegaki-ctrlgrp",e&&(a.id="tegaki-ctrlgrp-"+e),t!==undefined&&((i=$T.el("div")).className="tegaki-ctrlgrp-title",i.textContent=t,a.appendChild(i)),a},buildLayersCtrlGroup:function(){var e,t,a,i;return t=this.buildCtrlGroup("layers",TegakiStrings.layers),(a=$T.el("div")).id="tegaki-layers-opts",(i=$T.el("div")).id="tegaki-layer-alpha-cell",(e=$T.el("span")).className="tegaki-label-xs tegaki-lbl-c tegaki-drag-lbl",e.textContent=TegakiStrings.alpha,$T.on(e,"pointerdown",Tegaki.onLayerAlphaDragStart),i.appendChild(e),(e=$T.el("input")).id="tegaki-layer-alpha-opt",e.className="tegaki-stealth-input tegaki-range-lbl-xs",e.setAttribute("maxlength",3),$T.on(e,"input",Tegaki.onLayerAlphaChange),i.appendChild(e),a.appendChild(i),t.appendChild(a),(e=$T.el("div")).id="tegaki-layers-grid",t.appendChild(e),(a=$T.el("div")).id="tegaki-layers-ctrl",(e=$T.el("span")).title=TegakiStrings.addLayer,e.className="tegaki-ui-btn tegaki-icon tegaki-plus",$T.on(e,"click",Tegaki.onLayerAddClick),a.appendChild(e),(e=$T.el("span")).title=TegakiStrings.delLayers,e.className="tegaki-ui-btn tegaki-icon tegaki-minus",$T.on(e,"click",Tegaki.onLayerDeleteClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-merge",e.title=TegakiStrings.mergeLayers,e.className="tegaki-ui-btn tegaki-icon tegaki-level-down",$T.on(e,"click",Tegaki.onMergeLayersClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-up",e.title=TegakiStrings.moveLayerUp,e.setAttribute("data-up","1"),e.className="tegaki-ui-btn tegaki-icon tegaki-up-open",$T.on(e,"click",Tegaki.onMoveLayerClick),a.appendChild(e),(e=$T.el("span")).id="tegaki-layer-down",e.title=TegakiStrings.moveLayerDown,e.className="tegaki-ui-btn tegaki-icon tegaki-down-open",$T.on(e,"click",Tegaki.onMoveLayerClick),a.appendChild(e),t.appendChild(a),t},buildSizeCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("size",TegakiStrings.size),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-size",e.className="tegaki-ctrl-range",e.min=1,e.max=Tegaki.maxSize,e.type="range",e.title=TegakiKeybinds.getCaption("toolSize"),$T.on(e,"input",Tegaki.onToolSizeChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-size-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolSizeChange),a.appendChild(e),t.appendChild(a),t},buildAlphaCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("alpha",TegakiStrings.alpha),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-alpha",e.className="tegaki-ctrl-range",e.min=0,e.max=100,e.step=1,e.type="range",$T.on(e,"input",Tegaki.onToolAlphaChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-alpha-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolAlphaChange),a.appendChild(e),t.appendChild(a),t},buildFlowCtrlGroup:function(){var e,t,a;return t=this.buildCtrlGroup("flow",TegakiStrings.flow),(a=$T.el("div")).className="tegaki-ctrlrow",(e=$T.el("input")).id="tegaki-flow",e.className="tegaki-ctrl-range",e.min=0,e.max=100,e.step=1,e.type="range",$T.on(e,"input",Tegaki.onToolFlowChange),a.appendChild(e),(e=$T.el("input")).id="tegaki-flow-lbl",e.setAttribute("maxlength",3),e.className="tegaki-stealth-input tegaki-range-lbl",$T.on(e,"input",Tegaki.onToolFlowChange),a.appendChild(e),t.appendChild(a),t},buildZoomCtrlGroup:function(){var e,t,a;return a=this.buildCtrlGroup("zoom",TegakiStrings.zoom),(t=$T.el("div")).className="tegaki-ui-btn tegaki-icon tegaki-plus",t.id="tegaki-zoomin-btn",t.setAttribute("data-in",1),$T.on(t,"click",Tegaki.onZoomChange),a.appendChild(t),(t=$T.el("div")).className="tegaki-ui-btn tegaki-icon tegaki-minus",t.id="tegaki-zoomout-btn",t.setAttribute("data-out",1),$T.on(t,"click",Tegaki.onZoomChange),a.appendChild(t),(e=$T.el("div")).id="tegaki-zoom-lbl",a.appendChild(e),a},buildColorCtrlGroup:function(e){var t,a,i,s,r,n,o,l,g;for(n=/ Edge\//i.test(window.navigator.userAgent),s=this.buildCtrlGroup("color",TegakiStrings.color),(a=$T.el("div")).id="tegaki-color-ctrl",(t=$T.el("div")).id="tegaki-color",n&&t.classList.add("tegaki-hidden"),t.style.backgroundColor=e,$T.on(t,"mousedown",Tegaki.onMainColorClick),a.appendChild(t),(t=$T.el("div")).id="tegaki-palette-switcher",(i=$T.el("span")).id="tegaki-palette-prev-btn",i.title=TegakiStrings.switchPalette,i.setAttribute("data-prev","1"),i.className="tegaki-ui-btn tegaki-icon tegaki-left-open tegaki-disabled",$T.on(i,"click",Tegaki.onSwitchPaletteClick),t.appendChild(i),(i=$T.el("span")).id="tegaki-palette-next-btn",i.title=TegakiStrings.switchPalette,i.className="tegaki-ui-btn tegaki-icon tegaki-right-open",$T.on(i,"click",Tegaki.onSwitchPaletteClick),t.appendChild(i),a.appendChild(t),s.appendChild(a),(a=$T.el("div")).id="tegaki-color-grids",o=0;o0&&(g+=" tegaki-hidden"),t.className=g;for(r of l)(i=$T.el("div")).title=TegakiStrings.paletteSlotReplace,i.className="tegaki-color-btn",i.setAttribute("data-color",r),i.style.backgroundColor=r,$T.on(i,"mousedown",Tegaki.onPaletteColorClick),t.appendChild(i);a.appendChild(t)}return s.appendChild(a),(t=$T.el("input")).id="tegaki-colorpicker",!n&&t.classList.add("tegaki-invis"),t.value=r,t.type="color",$T.on(t,"change",Tegaki.onColorPicked),s.appendChild(t),s},buildStatusCnt:function(){var e,t;return(e=$T.el("div")).id="tegaki-status-cnt",Tegaki.saveReplay&&((t=$T.el("div")).id="tegaki-status-replay",t.textContent="\u2b24",t.setAttribute("title",TegakiStrings.recordingEnabled),e.appendChild(t)),(t=$T.el("div")).id="tegaki-status-output",e.appendChild(t),(t=$T.el("div")).id="tegaki-status-version",t.textContent="tegaki.js v"+Tegaki.VERSION,e.appendChild(t),e},buildReplayControls:function(){var e,t,a;return(e=$T.el("div")).id="tegaki-replay-controls",e.className="tegaki-hidden",(t=$T.el("span")).id="tegaki-replay-gapless-btn",t.className="tegaki-ui-cb-w",$T.on(t,"click",Tegaki.onReplayGaplessClick),(a=$T.el("span")).id="tegaki-replay-gapless-cb",a.className="tegaki-ui-cb",t.appendChild(a),(a=$T.el("span")).className="tegaki-menu-lbl",a.textContent=TegakiStrings.gapless,t.appendChild(a),e.appendChild(t),(t=$T.el("span")).id="tegaki-replay-play-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-play",t.setAttribute("title",TegakiStrings.play),$T.on(t,"click",Tegaki.onReplayPlayPauseClick),e.appendChild(t),(t=$T.el("span")).className="tegaki-ui-btn tegaki-icon tegaki-to-start",t.setAttribute("title",TegakiStrings.rewind),$T.on(t,"click",Tegaki.onReplayRewindClick),e.appendChild(t),(t=$T.el("span")).id="tegaki-replay-slower-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-fast-bw",t.setAttribute("title",TegakiStrings.slower),$T.on(t,"click",Tegaki.onReplaySlowDownClick),e.appendChild(t),(a=$T.el("span")).id="tegaki-replay-speed-lbl",a.className="tegaki-menu-lbl",a.textContent="1.0",e.appendChild(a),(t=$T.el("span")).id="tegaki-replay-faster-btn",t.className="tegaki-ui-btn tegaki-icon tegaki-fast-fw",t.setAttribute("title",TegakiStrings.faster),$T.on(t,"click",Tegaki.onReplaySpeedUpClick),e.appendChild(t),(a=$T.el("span")).id="tegaki-replay-now-lbl",a.className="tegaki-menu-lbl",a.textContent="00:00",e.appendChild(a),(a=$T.el("span")).id="tegaki-replay-end-lbl",a.className="tegaki-menu-lbl",a.textContent="00:00",e.appendChild(a),e},buildLayerGridCell:function(e){var t,a,i;return(t=$T.el("div")).id="tegaki-layers-cell-"+e.id,t.className="tegaki-layers-cell",t.setAttribute("data-id",e.id),t.draggable=!0,t.setAttribute("data-id",e.id),$T.on(t,"pointerdown",TegakiUI.onLayerSelectorPtrDown),$T.on(t,"pointerup",Tegaki.onLayerSelectorClick),$T.on(t,"dragstart",TegakiUI.onLayerDragStart),$T.on(t,"dragover",TegakiUI.onLayerDragOver),$T.on(t,"drop",TegakiUI.onLayerDragDrop),$T.on(t,"dragend",TegakiUI.onLayerDragEnd),$T.on(t,"dragleave",TegakiUI.onLayerDragLeave),$T.on(t,"dragexit",TegakiUI.onLayerDragLeave),(i=$T.el("div")).className="tegaki-layers-cell-v",(a=$T.el("span")).id="tegaki-layers-cb-v-"+e.id,a.className="tegaki-ui-cb",a.setAttribute("data-id",e.id),a.title=TegakiStrings.toggleVisibility,$T.on(a,"click",Tegaki.onLayerToggleVisibilityClick),e.visible&&(a.className+=" tegaki-ui-cb-a"),i.appendChild(a),t.appendChild(i),(i=$T.el("div")).className="tegaki-layers-cell-p",(a=$T.el("canvas")).id="tegaki-layers-p-canvas-"+e.id,a.className="tegaki-alpha-bg-xs",[a.width,a.height]=TegakiUI.getLayerPreviewSize(),i.appendChild(a),t.appendChild(i),(i=$T.el("div")).className="tegaki-layers-cell-n",(a=$T.el("div")).id="tegaki-layer-name-"+e.id,a.className="tegaki-ellipsis",a.setAttribute("data-id",e.id),a.textContent=e.name,$T.on(a,"dblclick",Tegaki.onLayerNameChangeClick),i.appendChild(a),t.appendChild(i),t},onLayerSelectorPtrDown:function(e){"mouse"===e.pointerType?this.hasAttribute("data-nodrag")&&(this.removeAttribute("data-nodrag"),$T.on(this,"dragstart",TegakiUI.onLayerDragStart)):this.hasAttribute("data-nodrag")||(this.setAttribute("data-nodrag",1),$T.off(this,"dragstart",TegakiUI.onLayerDragStart))},onLayerDragStart:function(e){var t,a;e.ctrlKey||(TegakiUI.draggedNode=null,$T.id("tegaki-layers-grid").children[1]?(a=+e.target.getAttribute("data-id"),(t=$T.el("div")).className="tegaki-invis",e.dataTransfer.setDragImage(t,0,0),e.dataTransfer.setData("text/plain",a),e.dataTransfer.effectAllowed="move",TegakiUI.draggedNode=e.target,TegakiUI.updateLayersGridDragExt(!0)):e.preventDefault())},onLayerDragOver:function(e){e.preventDefault(),e.dataTransfer.dropEffect="move",TegakiUI.updateLayersGridDragEffect(e.target,+TegakiUI.draggedNode.getAttribute("data-id"))},onLayerDragLeave:function(){TegakiUI.updateLayersGridDragEffect()},onLayerDragEnd:function(){TegakiUI.draggedNode=null,TegakiUI.updateLayersGridDragExt(!1),TegakiUI.updateLayersGridDragEffect()},onLayerDragDrop:function(e){var t,a,i;e.preventDefault(),TegakiUI.draggedNode=null,[t]=TegakiUI.layersGridFindDropTgt(e.target),a=+e.dataTransfer.getData("text/plain"),TegakiUI.updateLayersGridDragEffect(e.target.parentNode),TegakiUI.updateLayersGridDragExt(!1),TegakiUI.layersGridCanDrop(t,a)&&(i=t?TegakiLayers.getLayerPosById(t):Tegaki.layers.length,TegakiLayers.selectedLayersHas(a)||Tegaki.setActiveLayer(a),Tegaki.moveSelectedLayers(i))},updateLayersGridDragExt:function(e){var t,a;(t=$T.id("tegaki-layers-grid")).children[1]&&(e?((a=$T.el("div")).id="tegaki-layers-cell-dx",a.draggable=!0,$T.on(a,"dragover",TegakiUI.onLayerDragOver),$T.on(a,"drop",TegakiUI.onLayerDragDrop),t.parentNode.insertBefore(a,t)):(a=$T.id("tegaki-layers-cell-dx"))&&a.parentNode.removeChild(a))},updateLayersGridDragEffect:function(e,t){var a,i,s;i=$T.cls("tegaki-layers-cell-d",$T.id("tegaki-ctrlgrp-layers"));for(a of i)a.classList.remove("tegaki-layers-cell-d");e&&t&&([s,e]=TegakiUI.layersGridFindDropTgt(e),TegakiUI.layersGridCanDrop(s,t)&&(e||(e=$T.id("tegaki-layers-grid")),e.classList.add("tegaki-layers-cell-d")))},layersGridFindDropTgt:function(e){var t,a;for(t=+e.getAttribute("data-id"),a=$T.id("tegaki-ctrlgrp-layers");!e.draggable&&e!==a;)t=+(e=e.parentNode).getAttribute("data-id");return e!==a&&e.draggable?[t,e]:[0,null]},layersGridCanDrop:function(e,t){var a;if(e===t)return!1;if((a=$T.id("tegaki-layers-cell-"+t)).previousElementSibling){if(+a.previousElementSibling.getAttribute("data-id")===e)return!1}else if(!e)return!1;return!0},setReplayMode:function(e){Tegaki.bg.classList[e?"add":"remove"]("tegaki-replay-mode")},onToolChanged:function(){$T.id("tegaki-toolmode-bar").classList.remove("tegaki-hidden"),TegakiUI.updateToolSize(),TegakiUI.updateToolAlpha(),TegakiUI.updateToolFlow(),TegakiUI.updateToolModes()},updateLayerAlphaOpt:function(){$T.id("tegaki-layer-alpha-opt").value=Math.round(100*Tegaki.activeLayer.alpha)},updateLayerName:function(e){var t;(t=$T.id("tegaki-layer-name-"+e.id))&&(t.textContent=e.name)},updateLayerPreview:function(e){var t,a;(t=$T.id("tegaki-layers-p-canvas-"+e.id))&&((a=TegakiUI.getLayerPreviewCtx(e))||((a=t.getContext("2d")).imageSmoothingEnabled=!1,TegakiUI.setLayerPreviewCtx(e,a)),$T.clearCtx(a),a.drawImage(e.canvas,0,0,t.width,t.height))},updateLayerPreviewSize:function(e){var t,a,i;i=TegakiUI.getLayerPreviewSize();for(a of Tegaki.layers)(t=$T.id("tegaki-layers-p-canvas-"+a.id))&&([t.width,t.height]=i,e&&TegakiUI.updateLayerPreview(a))},getLayerPreviewCtx:function(e){TegakiUI.layerPreviewCtxCache.get(e)},setLayerPreviewCtx:function(e,t){TegakiUI.layerPreviewCtxCache.set(e,t)},deleteLayerPreviewCtx:function(e){TegakiUI.layerPreviewCtxCache["delete"](e)},updateLayersGridClear:function(){$T.id("tegaki-layers-grid").innerHTML=""},updateLayersGrid:function(){var e,t,a,i;a=$T.frag();for(e of Tegaki.layers)t=TegakiUI.buildLayerGridCell(e),a.insertBefore(t,a.firstElementChild);TegakiUI.updateLayersGridClear(),i.appendChild(a)},updateLayersGridActive:function(e){var t;(t=$T.cls("tegaki-layers-cell-a",$T.id("tegaki-layers-grid"))[0])&&t.classList.remove("tegaki-layers-cell-a"),(t=$T.id("tegaki-layers-cell-"+e))&&t.classList.add("tegaki-layers-cell-a"),TegakiUI.updateLayerAlphaOpt()},updateLayersGridAdd:function(e,t){var a,i,s;a=TegakiUI.buildLayerGridCell(e),i=$T.id("tegaki-layers-grid"),s=t?$T.id("tegaki-layers-cell-"+t):null,i.insertBefore(a,s)},updateLayersGridRemove:function(e){var t;(t=$T.id("tegaki-layers-cell-"+e))&&t.parentNode.removeChild(t)},updayeLayersGridOrder:function(){var e,t,a;t=$T.id("tegaki-layers-grid");for(e of Tegaki.layers)a=$T.id("tegaki-layers-cell-"+e.id),t.insertBefore(a,t.firstElementChild)},updateLayersGridVisibility:function(e,t){var a;(a=$T.id("tegaki-layers-cb-v-"+e))&&(t?a.classList.add("tegaki-ui-cb-a"):a.classList.remove("tegaki-ui-cb-a"))},updateLayersGridSelectedClear:function(){var e,t;for(e of Tegaki.layers)(t=$T.id("tegaki-layers-cell-"+e.id))&&t.classList.remove("tegaki-layers-cell-s")},updateLayersGridSelectedSet:function(e,t){var a;(a=$T.id("tegaki-layers-cell-"+e))&&(t?a.classList.add("tegaki-layers-cell-s"):a.classList.remove("tegaki-layers-cell-s"))},updateToolSize:function(){var e=$T.id("tegaki-ctrlgrp-size");Tegaki.tool.useSize?(e.classList.remove("tegaki-hidden"),$T.id("tegaki-size-lbl").value=Tegaki.tool.size,$T.id("tegaki-size").value=Tegaki.tool.size):e.classList.add("tegaki-hidden")},updateToolAlpha:function(){var e,t=$T.id("tegaki-ctrlgrp-alpha");Tegaki.tool.useAlpha?(t.classList.remove("tegaki-hidden"),e=Math.round(100*Tegaki.tool.alpha),$T.id("tegaki-alpha-lbl").value=e,$T.id("tegaki-alpha").value=e):t.classList.add("tegaki-hidden")},updateToolFlow:function(){var e,t=$T.id("tegaki-ctrlgrp-flow");Tegaki.tool.useFlow?(t.classList.remove("tegaki-hidden"),e=Math.round(100*Tegaki.tool.flow),$T.id("tegaki-flow-lbl").value=e,$T.id("tegaki-flow").value=e):t.classList.add("tegaki-hidden")},updateToolDynamics:function(){var e,t;e=$T.id("tegaki-tool-mode-dynamics"),Tegaki.tool.usesDynamics()?(t=$T.id("tegaki-tool-mode-dynamics-size"),Tegaki.tool.useSizeDynamics?(Tegaki.tool.sizeDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),t=$T.id("tegaki-tool-mode-dynamics-alpha"),Tegaki.tool.useAlphaDynamics?(Tegaki.tool.alphaDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),t=$T.id("tegaki-tool-mode-dynamics-flow"),Tegaki.tool.useFlowDynamics?(Tegaki.tool.flowDynamicsEnabled?t.classList.add("tegaki-sw-btn-a"):t.classList.remove("tegaki-sw-btn-a"),t.classList.remove("tegaki-hidden")):t.classList.add("tegaki-hidden"),e.classList.remove("tegaki-hidden")):e.classList.add("tegaki-hidden")},updateToolShape:function(){var e,t,a,i,s;if(t=$T.id("tegaki-tool-mode-tip"),Tegaki.tool.tipList){for(s=Tegaki.tool.tipList,(a=$T.id("tegaki-tool-mode-tip-ctrl")).innerHTML="",e=0;e=Tegaki.zoomFactorList.length?$T.id("tegaki-zoomin-btn").classList.add("tegaki-disabled"):$T.id("tegaki-zoomin-btn").classList.remove("tegaki-disabled"),Tegaki.zoomLevel+Tegaki.zoomBaseLevel<=0?$T.id("tegaki-zoomout-btn").classList.add("tegaki-disabled"):$T.id("tegaki-zoomout-btn").classList.remove("tegaki-disabled")},updateColorPalette:function(){var e,t,a;a=Tegaki.colorPaletteId,t=$T.cls("tegaki-color-grid",$T.id("tegaki-color-grids"));for(e of t)+e.getAttribute("data-id")===a?e.classList.remove("tegaki-hidden"):e.classList.add("tegaki-hidden");e=$T.id("tegaki-palette-prev-btn"),0===a?e.classList.add("tegaki-disabled"):e.classList.remove("tegaki-disabled"),e=$T.id("tegaki-palette-next-btn"),a===TegakiColorPalettes.length-1?e.classList.add("tegaki-disabled"):e.classList.remove("tegaki-disabled")},updateReplayTime:function(e){var t,a,i=Tegaki.replayViewer;(t=i.getCurrentPos())>(a=i.getDuration())&&(t=a),$T.id("tegaki-replay-now-lbl").textContent=$T.msToHms(t),e&&($T.id("tegaki-replay-end-lbl").textContent=$T.msToHms(a))},updateReplayControls:function(){TegakiUI.updateReplayGapless(),TegakiUI.updateReplayPlayPause(),TegakiUI.updateReplaySpeed()},updateReplayGapless:function(){var e,t=Tegaki.replayViewer;e=$T.id("tegaki-replay-gapless-cb"),t.gapless?e.classList.add("tegaki-ui-cb-a"):e.classList.remove("tegaki-ui-cb-a")},updateReplayPlayPause:function(){var e,t=Tegaki.replayViewer;e=$T.id("tegaki-replay-play-btn"),t.playing?(e.classList.remove("tegaki-play"),e.classList.add("tegaki-pause"),e.setAttribute("title",TegakiStrings.pause)):(e.classList.add("tegaki-play"),e.classList.remove("tegaki-pause"),e.setAttribute("title",TegakiStrings.play),t.getCurrentPos()>8&255},readUint:function(e,t){return 16777216*e[t+3]+(e[t+2]<<16|e[t+1]<<8|e[t])},writeUint:function(e,t,a){e[t]=255&a,e[t+1]=a>>8&255,e[t+2]=a>>16&255,e[t+3]=a>>24&255},readASCII:function(e,t,a){for(var i="",s=0;s>6,e[t+s+1]=128|n>>0&63,s+=2;else if(0==(4294901760&n))e[t+s]=224|n>>12,e[t+s+1]=128|n>>6&63,e[t+s+2]=128|n>>0&63,s+=3;else{if(0!=(4292870144&n))throw"e";e[t+s]=240|n>>18,e[t+s+1]=128|n>>12&63,e[t+s+2]=128|n>>6&63,e[t+s+3]=128|n>>0&63,s+=4}}return s},sizeUTF8:function(e){for(var t=e.length,a=0,i=0;i>>3}var h=r.lits,p=r.strt,k=r.prev,T=0,u=0,y=0,f=0,v=0,m=0;c>2&&(p[m=UZIP.F._hash(e,0)]=0);for(l=0;l14e3||u>26697)&&c-l>100&&(d>>16,w=65535&L;if(0!=L){w=65535&L;var S=n(C=L>>>16,r.of0);r.lhst[257+S]++;var I=n(w,r.df0);r.dhst[I]++,f+=r.exb[S]+r.dxb[I],h[T]=C<<23|l-d,h[T+1]=w<<16|S<<8|I,T+=2,d=l+C}else r.lhst[e[l]]++;u++}}for(y==l&&0!=e.length||(d>>3},UZIP.F._bestMatch=function(e,t,a,i,s,r){var n=32767&t,o=a[n],l=n-o+32768&32767;if(o==n||i!=UZIP.F._hash(e,t-l))return 0;for(var g=0,d=0,c=Math.min(32767,t);l<=c&&0!=--r&&o!=n;){if(0==g||e[t+g]==e[t+g-l]){var h=UZIP.F._howLong(e,t,l);if(h>g){if(d=l,(g=h)>=s)break;l+2p&&(p=u,o=T)}}}l+=(n=o)-(o=a[n])+32768&32767}return g<<16|d},UZIP.F._howLong=function(e,t,a){if(e[t]!=e[t-a]||e[t+1]!=e[t+1-a]||e[t+2]!=e[t+2-a])return 0;var i=t,s=Math.min(e.length,t+258);for(t+=3;t>>23,z=A+(8388607&E);A>16,M=x>>8&255,R=255&x;m(o,l=UZIP.F._writeLit(257+M,I,o,l),$-f.of0[M]),l+=f.exb[M],v(o,l=UZIP.F._writeLit(R,U,o,l),F-f.df0[R]),l+=f.dxb[R],A+=$}}l=UZIP.F._writeLit(256,I,o,l)}return l},UZIP.F._copyExact=function(e,t,a,i,s){var r=s>>>3;return i[r]=a,i[r+1]=a>>>8,i[r+2]=255-i[r],i[r+3]=255-i[r+1],r+=4,i.set(new Uint8Array(e.buffer,t,a),r),s+(a+4<<3)},UZIP.F.getTrees=function(){for(var e=UZIP.F.U,t=UZIP.F._hufTree(e.lhst,e.ltree,15),a=UZIP.F._hufTree(e.dhst,e.dtree,15),i=[],s=UZIP.F._lenCodes(e.ltree,i),r=[],n=UZIP.F._lenCodes(e.dtree,r),o=0;o4&&0==e.itree[1+(e.ordr[g-1]<<1)];)g--;return[t,a,l,s,n,g,i,r]},UZIP.F.getSecond=function(e){for(var t=[],a=0;a>1)+",");return t},UZIP.F.contSize=function(e,t){for(var a=0,i=0;i15&&(UZIP.F._putsE(a,i,n,o),i+=o)}return i},UZIP.F._lenCodes=function(e,t){for(var a=e.length;2!=a&&0==e[a-1];)a-=2;for(var i=0;i>>1,138))<11?t.push(17,g-3):t.push(18,g-11),i+=2*g-2}else if(s==o&&r==s&&n==s){for(l=i+5;l+2>>1,6);t.push(16,g-3),i+=2*g-2}else t.push(s,0)}return a>>>1},UZIP.F._hufTree=function(e,t,a){var i=[],s=e.length,r=t.length,n=0;for(n=0;na&&(UZIP.F.restrictDepth(l,a,T),T=a),n=0;nt;i++){var n=e[i].d;e[i].d=t,r+=s-(1<>>=a-t;r>0;){(n=e[i].d)=0;i--)e[i].d==t&&r<0&&(e[i].d--,r++)},UZIP.F._goodIndex=function(e,t){var a=0;return t[16|a]<=e&&(a|=16),t[8|a]<=e&&(a|=8),t[4|a]<=e&&(a|=4),t[2|a]<=e&&(a|=2),t[1|a]<=e&&(a|=1),a},UZIP.F._writeLit=function(e,t,a,i){return UZIP.F._putsF(a,i,t[e<<1]),i+t[1+(e<<1)]},UZIP.F.inflate=function(e,t){var a=Uint8Array;if(3==e[0]&&0==e[1])return t||new a(0);var i=UZIP.F,s=i._bitsF,r=i._bitsE,n=i._decodeTiny,o=i.makeCodes,l=i.codes2map,g=i._get17,d=i.U,c=null==t;c&&(t=new a(e.length>>>2<<3));for(var h,p,k=0,T=0,u=0,y=0,f=0,v=0,m=0,b=0,L=0;0==k;)if(k=s(e,L,1),T=s(e,L+1,2),L+=3,0!=T){if(c&&(t=UZIP.F._check(t,b+(1<<17))),1==T&&(h=d.flmap,p=d.fdmap,v=511,m=31),2==T){u=r(e,L,5)+257,y=r(e,L+5,5)+1,f=r(e,L+10,4)+4;L+=14;for(var C=0;C<38;C+=2)d.itree[C]=0,d.itree[C+1]=0;var w=1;for(C=0;Cw&&(w=S)}L+=3*f,o(d.itree,w),l(d.itree,w,d.imap),h=d.lmap,p=d.dmap,L=n(d.imap,(1<>>4;if(A>>>8==0)t[b++]=A;else{if(256==A)break;var D=b+A-254;if(A>264){var E=d.ldef[A-257];D=b+(E>>>3)+r(e,L,7&E),L+=7&E}var $=p[g(e,L)&m];L+=15&$;var z=$>>>4,x=d.ddef[z],F=(x>>>4)+s(e,L,15&x);for(L+=15&x,c&&(t=UZIP.F._check(t,b+(1<<17)));b>>3),R=e[M-4]|e[M-3]<<8;c&&(t=UZIP.F._check(t,b+R)),t.set(new a(e.buffer,e.byteOffset+M,R),b),L=M+R<<3,b+=R}return t.length==b?t:t.slice(0,b)},UZIP.F._check=function(e,t){var a=e.length;if(t<=a)return e;var i=new Uint8Array(Math.max(a<<1,t));return i.set(e,0),i},UZIP.F._decodeTiny=function(e,t,a,i,s,r){for(var n=UZIP.F._bitsE,o=UZIP.F._get17,l=0;l>>4;if(d<=15)r[l]=d,l++;else{var c=0,h=0;16==d?(h=3+n(i,s,2),s+=2,c=r[l-1]):17==d?(h=3+n(i,s,3),s+=3):18==d&&(h=11+n(i,s,7),s+=7);for(var p=l+h;l>>1;rs&&(s=o),r++}for(;r>1,o=e[r+1],l=n<<4|o,g=t-o,d=e[r]<>>15-t]=l,d++}},UZIP.F.revCodes=function(e,t){for(var a=UZIP.F.U.rev15,i=15-t,s=0;s>>i}},UZIP.F._putsE=function(e,t,a){a<<=7&t;var i=t>>>3;e[i]|=a,e[i+1]|=a>>>8},UZIP.F._putsF=function(e,t,a){a<<=7&t;var i=t>>>3;e[i]|=a,e[i+1]|=a>>>8,e[i+2]|=a>>>16},UZIP.F._bitsE=function(e,t,a){return(e[t>>>3]|e[1+(t>>>3)]<<8)>>>(7&t)&(1<>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)&(1<>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16)>>>(7&t)},UZIP.F._get25=function(e,t){return(e[t>>>3]|e[1+(t>>>3)]<<8|e[2+(t>>>3)]<<16|e[3+(t>>>3)]<<24)>>>(7&t)},UZIP.F.U=function(){var e=Uint16Array,t=Uint32Array;return{next_code:new e(16),bl_count:new e(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new e(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new t(32),flmap:new e(512),fltree:[],fdmap:new e(32),fdtree:[],lmap:new e(32768),ltree:[],ttree:[],dmap:new e(32768),dtree:[],imap:new e(512),itree:[],rev15:new e(32768),lhst:new t(286),dhst:new t(30),ihst:new t(19),lits:new t(15e3),strt:new e(65536),prev:new e(32768)}}(),function(){function e(e,t,a){for(;0!=t--;)e.push(0,a)}for(var t=UZIP.F.U,a=32768,i=0;i>>1|(1431655765&s)<<1))>>>2|(858993459&s)<<2))>>>4|(252645135&s)<<4))>>>8|(16711935&s)<<8,t.rev15[i]=(s>>>16|s<<16)>>>17}for(i=0;i<32;i++)t.ldef[i]=t.of0[i]<<3|t.exb[i],t.ddef[i]=t.df0[i]<<4|t.dxb[i];e(t.fltree,144,8),e(t.fltree,112,9),e(t.fltree,24,7),e(t.fltree,8,8),UZIP.F.makeCodes(t.fltree,9),UZIP.F.codes2map(t.fltree,9,t.flmap),UZIP.F.revCodes(t.fltree,9),e(t.fdtree,32,5),UZIP.F.makeCodes(t.fdtree,5),UZIP.F.codes2map(t.fdtree,5,t.fdmap),UZIP.F.revCodes(t.fdtree,5),e(t.itree,19,0),e(t.ltree,286,0),e(t.dtree,30,0),e(t.ttree,320,0)}(); \ No newline at end of file diff --git a/scripts/sync-directories.js b/scripts/sync-directories.js index c7882b14..785e9f5e 100644 --- a/scripts/sync-directories.js +++ b/scripts/sync-directories.js @@ -41,6 +41,7 @@ const DIRECTORY_CODE_ORDER = [ 'wsg', 'diy', 'out', + 'i', 'ic', 'mu', 'int', @@ -259,7 +260,10 @@ const loadDirectoriesSource = async () => { throw new Error('Invalid GitHub directory listing'); } - const fileNames = contents.map((entry) => (isRecord(entry) ? entry.name : undefined)).filter((name) => typeof name === 'string' && isDirectoryListFile(name)).sort(); + const fileNames = contents + .map((entry) => (isRecord(entry) ? entry.name : undefined)) + .filter((name) => typeof name === 'string' && isDirectoryListFile(name)) + .sort(); const defaults = normalizeDirectoryDefaultsData(await fetchJson(`${GITHUB_RAW_BASE_URL}/${DEFAULTS_FILE_NAME}`)); const directories = await Promise.all( fileNames.map(async (fileName) => { diff --git a/src/components/oekaki-drawing-controls/__tests__/oekaki-drawing-controls.test.tsx b/src/components/oekaki-drawing-controls/__tests__/oekaki-drawing-controls.test.tsx new file mode 100644 index 00000000..ea8ec255 --- /dev/null +++ b/src/components/oekaki-drawing-controls/__tests__/oekaki-drawing-controls.test.tsx @@ -0,0 +1,370 @@ +import * as React from 'react'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import OekakiDrawingControls from '../oekaki-drawing-controls'; +import { OEKAKI_MOBILE_PORTRAIT_MESSAGE, OEKAKI_WEB_DOWNLOAD_MESSAGE } from '../../../lib/oekaki/oekaki-copy'; +import type { UploadedFileResult } from '../../../hooks/use-file-upload'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; + +const testState = vi.hoisted(() => ({ + runtime: 'web' as 'web' | 'electron' | 'android', + loadTegakiMock: vi.fn(), + openMock: vi.fn(), + flattenMock: vi.fn(), + destroyMock: vi.fn(), + onOpenImageLoadedMock: vi.fn(), +})); + +vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({ + getMediaHostingRuntime: () => testState.runtime, +})); + +vi.mock('../../../lib/oekaki/tegaki-loader', () => ({ + TEGAKI_DRAWING_FILE_NAME: 'tegaki.png', + loadTegaki: testState.loadTegakiMock, +})); + +let container: HTMLDivElement; +let root: Root; +const OriginalImage = globalThis.Image; + +interface MockTegakiOpenOptions { + width: number; + height: number; + saveReplay: boolean; + onDone: () => void; + onCancel: () => void; +} + +const setViewport = (width: number, height: number) => { + Object.defineProperty(window, 'innerWidth', { configurable: true, value: width }); + Object.defineProperty(window, 'innerHeight', { configurable: true, value: height }); + Object.defineProperty(window, 'matchMedia', { + configurable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: query === '(max-width: 640px) and (orientation: portrait)' && width <= 640 && height > width, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}; + +const createFinishedCanvas = () => { + const canvas = document.createElement('canvas'); + Object.defineProperty(canvas, 'toBlob', { + configurable: true, + value: (callback: BlobCallback) => callback(new Blob(['png'], { type: 'image/png' })), + }); + return canvas; +}; + +class MockImage { + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + + set src(_value: string) { + queueMicrotask(() => this.onload?.()); + } +} + +const renderControls = async ({ + uploadFile = vi.fn<(file: File) => Promise>().mockResolvedValue(null), + onClearUploadedUrl = vi.fn(), +}: { + uploadFile?: (file: File) => Promise; + onClearUploadedUrl?: (url: string) => void; +} = {}) => { + await act(async () => { + root.render(createElement(OekakiDrawingControls, { uploadFile, onClearUploadedUrl })); + }); + return { uploadFile, onClearUploadedUrl }; +}; + +const getButton = (label: string): HTMLButtonElement => { + const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === label); + if (!(button instanceof HTMLButtonElement)) { + throw new Error(`Button ${label} not found`); + } + return button; +}; + +const clickButton = async (label: string) => { + const button = getButton(label); + await act(async () => { + button.dispatchEvent(new MouseEvent('click', { bubbles: true })); + }); +}; + +const triggerTegakiDone = async () => { + const openOptions = testState.openMock.mock.calls.at(-1)?.[0] as MockTegakiOpenOptions | undefined; + if (!openOptions) { + throw new Error('Tegaki open options not captured'); + } + await act(async () => { + openOptions.onDone(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); +}; + +describe('OekakiDrawingControls', () => { + beforeEach(() => { + vi.clearAllMocks(); + testState.runtime = 'web'; + testState.openMock.mockReset(); + testState.flattenMock.mockReset(); + testState.destroyMock.mockReset(); + testState.onOpenImageLoadedMock.mockReset(); + testState.flattenMock.mockReturnValue(createFinishedCanvas()); + const tegaki = { + bg: null as HTMLElement | null, + open: testState.openMock, + flatten: testState.flattenMock, + destroy: testState.destroyMock, + onOpenImageLoaded: testState.onOpenImageLoadedMock, + }; + testState.openMock.mockImplementation(() => { + tegaki.bg = document.createElement('div'); + }); + testState.destroyMock.mockImplementation(() => { + tegaki.bg = null; + }); + testState.loadTegakiMock.mockResolvedValue(tegaki); + Object.defineProperty(globalThis, 'Image', { + configurable: true, + value: MockImage, + }); + Object.defineProperty(globalThis, 'alert', { + configurable: true, + value: vi.fn(), + writable: true, + }); + Object.defineProperty(globalThis, 'confirm', { + configurable: true, + value: vi.fn(() => true), + writable: true, + }); + Object.defineProperty(URL, 'createObjectURL', { + configurable: true, + value: vi.fn(() => 'blob:tegaki'), + }); + Object.defineProperty(URL, 'revokeObjectURL', { + configurable: true, + value: vi.fn(), + }); + vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined); + setViewport(1024, 768); + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + Object.defineProperty(globalThis, 'Image', { + configurable: true, + value: OriginalImage, + }); + vi.restoreAllMocks(); + }); + + it('alerts instead of opening Tegaki on a portrait phone viewport', async () => { + setViewport(390, 844); + + await renderControls(); + await clickButton('Draw'); + + expect(globalThis.alert).toHaveBeenCalledWith(OEKAKI_MOBILE_PORTRAIT_MESSAGE); + expect(testState.loadTegakiMock).not.toHaveBeenCalled(); + }); + + it('opens Tegaki on a landscape phone viewport', async () => { + setViewport(844, 390); + + await renderControls(); + await clickButton('Draw'); + + expect(globalThis.alert).not.toHaveBeenCalled(); + expect(testState.loadTegakiMock).toHaveBeenCalledTimes(1); + expect(testState.openMock).toHaveBeenCalledWith(expect.objectContaining({ width: 400, height: 400, saveReplay: true })); + }); + + it('keeps Draw disabled while Tegaki is open', async () => { + await renderControls(); + await clickButton('Draw'); + + expect(getButton('Draw').disabled).toBe(true); + await clickButton('Draw'); + expect(testState.loadTegakiMock).toHaveBeenCalledTimes(1); + + const openOptions = testState.openMock.mock.calls.at(-1)?.[0] as MockTegakiOpenOptions; + await act(async () => { + openOptions.onCancel(); + }); + + expect(testState.destroyMock).toHaveBeenCalledTimes(1); + expect(getButton('Draw').disabled).toBe(false); + }); + + it('downloads the web drawing only after confirmation', async () => { + await renderControls(); + await clickButton('Draw'); + await triggerTegakiDone(); + + expect(globalThis.confirm).toHaveBeenCalledWith(OEKAKI_WEB_DOWNLOAD_MESSAGE); + expect(testState.destroyMock).toHaveBeenCalledTimes(1); + expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(1); + expect(URL.createObjectURL).toHaveBeenCalledOnce(); + }); + + it('keeps Draw disabled while a finished drawing is still exporting', async () => { + let finishExport: BlobCallback | null = null; + const canvas = document.createElement('canvas'); + Object.defineProperty(canvas, 'toBlob', { + configurable: true, + value: (callback: BlobCallback) => { + finishExport = callback; + }, + }); + testState.flattenMock.mockReturnValue(canvas); + + await renderControls(); + await clickButton('Draw'); + const openOptions = testState.openMock.mock.calls.at(-1)?.[0] as MockTegakiOpenOptions; + await act(async () => { + openOptions.onDone(); + await Promise.resolve(); + }); + + expect(getButton('Draw').disabled).toBe(true); + expect(testState.openMock).toHaveBeenCalledTimes(1); + + await act(async () => { + finishExport?.(new Blob(['png'], { type: 'image/png' })); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(getButton('Edit').disabled).toBe(false); + }); + + it('unlocks the controls when exporting the finished drawing fails', async () => { + const canvas = document.createElement('canvas'); + Object.defineProperty(canvas, 'toBlob', { + configurable: true, + value: (callback: BlobCallback) => callback(null), + }); + testState.flattenMock.mockReturnValue(canvas); + + await renderControls(); + await clickButton('Draw'); + const openOptions = testState.openMock.mock.calls.at(-1)?.[0] as MockTegakiOpenOptions; + await act(async () => { + openOptions.onDone(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(globalThis.alert).toHaveBeenCalledWith('Could not export drawing'); + expect(testState.destroyMock).toHaveBeenCalledTimes(1); + expect(getButton('Draw').disabled).toBe(false); + }); + + it('does not download the web drawing when confirmation is cancelled', async () => { + Object.defineProperty(globalThis, 'confirm', { + configurable: true, + value: vi.fn(() => false), + }); + + await renderControls(); + await clickButton('Draw'); + await triggerTegakiDone(); + + expect(globalThis.confirm).toHaveBeenCalledWith(OEKAKI_WEB_DOWNLOAD_MESSAGE); + expect(HTMLAnchorElement.prototype.click).not.toHaveBeenCalled(); + expect(URL.createObjectURL).not.toHaveBeenCalled(); + }); + + it('clears the uploaded drawing URL when Clear is clicked', async () => { + testState.runtime = 'electron'; + const uploadFile = vi.fn<(file: File) => Promise>().mockResolvedValue({ + url: 'https://files.example/tegaki.png', + fileName: 'tegaki.png', + }); + const onClearUploadedUrl = vi.fn(); + + await renderControls({ uploadFile, onClearUploadedUrl }); + await clickButton('Draw'); + await triggerTegakiDone(); + await clickButton('Clear'); + + expect(onClearUploadedUrl).toHaveBeenCalledWith('https://files.example/tegaki.png'); + }); + + it('clears a stale uploaded URL when re-uploading an edited drawing fails', async () => { + testState.runtime = 'electron'; + const uploadFile = vi + .fn<(file: File) => Promise>() + .mockResolvedValueOnce({ url: 'https://files.example/first.png', fileName: 'tegaki.png' }) + .mockResolvedValueOnce(null); + const onClearUploadedUrl = vi.fn(); + + await renderControls({ uploadFile, onClearUploadedUrl }); + await clickButton('Draw'); + await triggerTegakiDone(); + await clickButton('Edit'); + await triggerTegakiDone(); + + expect(uploadFile).toHaveBeenCalledTimes(2); + expect(onClearUploadedUrl).toHaveBeenCalledWith('https://files.example/first.png'); + }); + + it('starts edited drawings from a fresh Tegaki session with the saved image loaded', async () => { + testState.runtime = 'electron'; + const uploadFile = vi.fn<(file: File) => Promise>().mockResolvedValue({ + url: 'https://files.example/tegaki.png', + fileName: 'tegaki.png', + }); + + await renderControls({ uploadFile }); + await clickButton('Draw'); + await triggerTegakiDone(); + await clickButton('Edit'); + + expect(testState.openMock).toHaveBeenCalledTimes(2); + expect(testState.destroyMock).toHaveBeenCalledTimes(1); + expect(testState.onOpenImageLoadedMock).toHaveBeenCalledTimes(1); + expect(URL.createObjectURL).toHaveBeenCalledWith(expect.any(File)); + }); + + it('destroys Tegaki when an edited drawing cannot be loaded', async () => { + testState.runtime = 'electron'; + const uploadFile = vi.fn<(file: File) => Promise>().mockResolvedValue({ + url: 'https://files.example/tegaki.png', + fileName: 'tegaki.png', + }); + + await renderControls({ uploadFile }); + await clickButton('Draw'); + await triggerTegakiDone(); + testState.onOpenImageLoadedMock.mockImplementationOnce(() => { + throw new Error('Could not restore drawing'); + }); + await clickButton('Edit'); + + expect(testState.destroyMock).toHaveBeenCalledTimes(2); + expect(globalThis.alert).toHaveBeenCalledWith('Could not restore drawing'); + expect(getButton('Edit').disabled).toBe(false); + }); +}); diff --git a/src/components/oekaki-drawing-controls/index.ts b/src/components/oekaki-drawing-controls/index.ts new file mode 100644 index 00000000..2c65e6a0 --- /dev/null +++ b/src/components/oekaki-drawing-controls/index.ts @@ -0,0 +1 @@ +export { default } from './oekaki-drawing-controls'; diff --git a/src/components/oekaki-drawing-controls/oekaki-drawing-controls.module.css b/src/components/oekaki-drawing-controls/oekaki-drawing-controls.module.css new file mode 100644 index 00000000..4a4b599f --- /dev/null +++ b/src/components/oekaki-drawing-controls/oekaki-drawing-controls.module.css @@ -0,0 +1,33 @@ +.controls { + display: inline-flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + font-size: 10pt; + font-weight: normal; + text-transform: none; +} + +.controls input.sizeInput { + text-align: center; + width: 30px !important; +} + +.replayLabel { + display: inline-flex; + align-items: center; + gap: 2px; + white-space: nowrap; +} + +.replayLabel input { + margin: 0; +} + +.controls button { + position: static !important; + margin-left: 0 !important; + filter: var(--filter80); + cursor: pointer; + text-transform: capitalize; +} diff --git a/src/components/oekaki-drawing-controls/oekaki-drawing-controls.tsx b/src/components/oekaki-drawing-controls/oekaki-drawing-controls.tsx new file mode 100644 index 00000000..cb1f9f8f --- /dev/null +++ b/src/components/oekaki-drawing-controls/oekaki-drawing-controls.tsx @@ -0,0 +1,248 @@ +import { useRef, useState } from 'react'; +import { getMediaHostingRuntime } from '../../lib/media-hosting/show-upload-controls'; +import { loadTegaki, TEGAKI_DRAWING_FILE_NAME, type TegakiGlobal } from '../../lib/oekaki/tegaki-loader'; +import { OEKAKI_MOBILE_PORTRAIT_MESSAGE, OEKAKI_WEB_DOWNLOAD_MESSAGE } from '../../lib/oekaki/oekaki-copy'; +import type { UploadedFileResult } from '../../hooks/use-file-upload'; +import styles from './oekaki-drawing-controls.module.css'; + +const DEFAULT_DIMENSION = '400'; +const MIN_DIMENSION = 1; +const MAX_DIMENSION = 2000; +const PNG_MIME_TYPE = 'image/png'; + +interface OekakiDrawingControlsProps { + disabled?: boolean; + className?: string; + uploadFile: (file: File) => Promise; + onClearUploadedUrl: (url: string) => void; +} + +const parseDimension = (value: string): number => { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) return Number.parseInt(DEFAULT_DIMENSION, 10); + return Math.min(MAX_DIMENSION, Math.max(MIN_DIMENSION, parsed)); +}; + +const makeDrawingFile = (blob: Blob): File => new File([blob], TEGAKI_DRAWING_FILE_NAME, { type: PNG_MIME_TYPE }); + +const isPhonePortraitViewport = (): boolean => { + if (typeof window === 'undefined') return false; + if (typeof window.matchMedia === 'function') { + return window.matchMedia('(max-width: 640px) and (orientation: portrait)').matches; + } + return window.innerWidth <= 640 && window.innerHeight > window.innerWidth; +}; + +const canvasToBlob = (canvas: HTMLCanvasElement): Promise => + new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + return; + } + reject(new Error('Could not export drawing')); + }, PNG_MIME_TYPE); + }); + +const downloadDrawing = (file: File): void => { + const url = URL.createObjectURL(file); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = TEGAKI_DRAWING_FILE_NAME; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 0); +}; + +const loadDrawingImage = (file: File | null): Promise => + new Promise((resolve, reject) => { + if (!file) { + resolve(null); + return; + } + + const url = URL.createObjectURL(file); + const image = new Image(); + image.onload = () => { + URL.revokeObjectURL(url); + resolve(image); + }; + image.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Could not load drawing')); + }; + image.src = url; + }); + +const destroyTegaki = (tegaki: TegakiGlobal): void => { + if (tegaki.bg && typeof tegaki.destroy === 'function') { + tegaki.destroy(); + } +}; + +const OekakiDrawingControls = ({ disabled = false, className, uploadFile, onClearUploadedUrl }: OekakiDrawingControlsProps) => { + const [width, setWidth] = useState(DEFAULT_DIMENSION); + const [height, setHeight] = useState(DEFAULT_DIMENSION); + const [saveReplay, setSaveReplay] = useState(true); + const [drawingFile, setDrawingFile] = useState(null); + const [isOpening, setIsOpening] = useState(false); + const [isTegakiOpen, setIsTegakiOpen] = useState(false); + const [isUploadingDrawing, setIsUploadingDrawing] = useState(false); + const uploadedDrawingUrlRef = useRef(null); + const tegakiSessionOpenRef = useRef(false); + const runtime = getMediaHostingRuntime(); + const isBusy = disabled || isOpening || isTegakiOpen || isUploadingDrawing; + const hasDrawing = drawingFile !== null; + + const closeTegakiSession = () => { + tegakiSessionOpenRef.current = false; + setIsTegakiOpen(false); + }; + + const handleDrawingFile = async (file: File) => { + const previousUploadedUrl = uploadedDrawingUrlRef.current; + setDrawingFile(file); + + if (runtime === 'web') { + uploadedDrawingUrlRef.current = null; + if (previousUploadedUrl) { + onClearUploadedUrl(previousUploadedUrl); + } + if (window.confirm(OEKAKI_WEB_DOWNLOAD_MESSAGE)) { + downloadDrawing(file); + } + return; + } + + setIsUploadingDrawing(true); + try { + const result = await uploadFile(file); + if (result?.url) { + uploadedDrawingUrlRef.current = result.url; + return; + } + uploadedDrawingUrlRef.current = null; + if (previousUploadedUrl) { + onClearUploadedUrl(previousUploadedUrl); + } + } catch (error) { + uploadedDrawingUrlRef.current = null; + if (previousUploadedUrl) { + onClearUploadedUrl(previousUploadedUrl); + } + throw error; + } finally { + setIsUploadingDrawing(false); + } + }; + + const openTegaki = async () => { + if (isBusy || tegakiSessionOpenRef.current) return; + if (isPhonePortraitViewport()) { + window.alert(OEKAKI_MOBILE_PORTRAIT_MESSAGE); + return; + } + + setIsOpening(true); + try { + const [tegaki, existingImage] = await Promise.all([loadTegaki(), loadDrawingImage(drawingFile)]); + destroyTegaki(tegaki); + tegakiSessionOpenRef.current = true; + setIsTegakiOpen(true); + tegaki.open({ + width: parseDimension(width), + height: parseDimension(height), + saveReplay, + onDone: () => { + const canvas = tegaki.flatten(); + setIsUploadingDrawing(true); + void canvasToBlob(canvas) + .then(makeDrawingFile) + .then(async (file) => { + destroyTegaki(tegaki); + closeTegakiSession(); + await handleDrawingFile(file); + }) + .catch((error) => { + destroyTegaki(tegaki); + closeTegakiSession(); + window.alert(error instanceof Error ? error.message : String(error)); + }) + .finally(() => { + setIsUploadingDrawing(false); + }); + }, + onCancel: () => { + destroyTegaki(tegaki); + closeTegakiSession(); + }, + }); + if (existingImage && typeof tegaki.onOpenImageLoaded === 'function') { + try { + tegaki.onOpenImageLoaded.call(existingImage); + } catch (error) { + destroyTegaki(tegaki); + throw error; + } + } + } catch (error) { + closeTegakiSession(); + window.alert(error instanceof Error ? error.message : String(error)); + } finally { + setIsOpening(false); + } + }; + + const clearDrawing = () => { + const uploadedUrl = uploadedDrawingUrlRef.current; + setDrawingFile(null); + uploadedDrawingUrlRef.current = null; + if (uploadedUrl) { + onClearUploadedUrl(uploadedUrl); + } + }; + + return ( +
+ Size + setWidth(event.target.value)} + /> + × + setHeight(event.target.value)} + /> + + + +
+ ); +}; + +export default OekakiDrawingControls; diff --git a/src/components/post-form/__tests__/post-form.test.tsx b/src/components/post-form/__tests__/post-form.test.tsx index 6c14258b..39496c80 100644 --- a/src/components/post-form/__tests__/post-form.test.tsx +++ b/src/components/post-form/__tests__/post-form.test.tsx @@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client'; import { Link, MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import PostForm, { LinkTypePreviewer } from '../post-form'; +import { OEKAKI_WEB_WARNING_TEXT } from '../../../lib/oekaki/oekaki-copy'; import { POST_OPTIONS_VALIDATION_DELAY_MS } from '../../../lib/utils/post-options-utils'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -24,10 +25,12 @@ const testState = vi.hoisted(() => ({ editedComment: undefined as { commentModeration?: { archived?: boolean }; deleted?: boolean; locked?: boolean; postCid?: string; removed?: boolean } | undefined, gifFrameStatus: 'idle' as 'idle' | 'ready', handleUploadMock: vi.fn(), + uploadFileMock: vi.fn(), isOffline: false, isOnlineStatusLoading: false, isUploading: false, isResolvingExternalQuotes: false, + mediaHostingRuntime: 'web' as 'web' | 'android' | 'electron', navigateMock: vi.fn(), offlineTitle: 'offline board', postIndex: undefined as number | undefined, @@ -259,6 +262,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({ testState.uploadComplete = onUploadComplete; return { handleUpload: testState.handleUploadMock, + uploadFile: testState.uploadFileMock, isUploading: testState.isUploading, uploadedFileName: testState.uploadedFileName, }; @@ -286,8 +290,9 @@ vi.mock('../../../lib/utils/media-utils', () => ({ })); vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({ + getMediaHostingRuntime: () => testState.mediaHostingRuntime, getShowUploadControls: () => testState.showUploadControls, - isWebRuntime: () => true, + isWebRuntime: () => testState.mediaHostingRuntime === 'web', })); vi.mock('../../../stores/use-media-hosting-store', () => ({ @@ -438,6 +443,7 @@ describe('PostForm', () => { testState.isOnlineStatusLoading = false; testState.isUploading = false; testState.isResolvingExternalQuotes = false; + testState.mediaHostingRuntime = 'web'; testState.offlineTitle = 'offline board'; testState.postIndex = undefined; testState.publishedPostOptions = undefined; @@ -456,6 +462,7 @@ describe('PostForm', () => { 'traditional-games.bso': { address: 'traditional-games.bso' }, }; testState.handleUploadMock.mockReset(); + testState.uploadFileMock.mockReset(); testState.navigateMock.mockReset(); testState.publishPostMock.mockReset(); testState.publishReplyMock.mockReset(); @@ -573,6 +580,43 @@ describe('PostForm', () => { expect(testState.setPublishPostOptionsMock).toHaveBeenCalledWith({ communityAddress: 'music-posting.eth' }); }); + it('shows Oekaki draw controls only on the /i/ board form', async () => { + testState.directories.push({ + address: 'oekaki-posting.bso', + directoryCode: 'i', + features: { requirePostLink: true, requirePostLinkIsMedia: true }, + title: '/i/ - Oekaki', + }); + testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' }; + testState.resolvedCommunityAddress = 'oekaki-posting.bso'; + + await renderPostForm('/i'); + await clickByText(container, 'start_new_thread'); + + const table = container.querySelector('table') as HTMLTableElement; + const drawRow = Array.from(table.querySelectorAll('tr')).find((row) => row.textContent?.includes('Size') && row.textContent?.includes('Replay')); + expect(table.textContent).toContain('Size'); + expect(table.textContent).toContain('Replay'); + expect(Array.from(table.querySelectorAll('span')).some((span) => span.textContent === '×')).toBe(true); + expect(drawRow?.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT); + expect(Array.from(table.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(true); + expect((Array.from(table.querySelectorAll('button')).find((button) => button.textContent === 'Clear') as HTMLButtonElement | undefined)?.disabled).toBe(true); + const rulesItems = Array.from(table.querySelectorAll('tr.rules li')).map((item) => item.textContent); + expect(rulesItems).toEqual(['Please read the Rules and FAQ before posting.', OEKAKI_WEB_WARNING_TEXT]); + + testState.mediaHostingRuntime = 'electron'; + await renderPostForm('/i'); + await clickByText(container, 'start_new_thread'); + + expect(container.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT); + + testState.resolvedCommunityAddress = 'music-posting.eth'; + await renderPostForm('/mu'); + await clickByText(container, 'start_new_thread'); + + expect(Array.from(container.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(false); + }); + it('drops stale thread content when board navigation remounts the form before a link-only post', async () => { await renderNavigablePostForm('/mu'); await clickByText(container, 'start_new_thread'); diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index a6b75e77..7190dc7f 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -36,11 +36,13 @@ import usePublishPost from '../../hooks/use-publish-post'; import usePublishReply from '../../hooks/use-publish-reply'; import { useFileUpload } from '../../hooks/use-file-upload'; import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls'; +import { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; import useMediaHostingStore from '../../stores/use-media-hosting-store'; import BoardOfflineAlert from '../board-offline-alert/board-offline-alert'; import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar'; import LoadingEllipsis from '../loading-ellipsis'; +import OekakiDrawingControls from '../oekaki-drawing-controls'; import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message'; import styles from './post-form.module.css'; import capitalize from 'lodash/capitalize'; @@ -141,6 +143,7 @@ interface PostFormFieldsProps { isUploading: boolean; uploadedFileName: string | null | undefined; showUploadControls: boolean; + showOekakiControls: boolean; showSpoilerForPost: boolean; showSpoilerForReply: boolean; isInAllView: boolean; @@ -158,6 +161,8 @@ interface PostFormFieldsProps { onPublishReply: () => void; onPublishPost: () => void; handleUpload: () => void; + uploadFile: ReturnType['uploadFile']; + onOekakiClearUploadedUrl: (url: string) => void; disableReplyPublish: boolean; } @@ -185,6 +190,7 @@ const PostFormFields = ({ isUploading, uploadedFileName, showUploadControls, + showOekakiControls, showSpoilerForPost, showSpoilerForReply, isInAllView, @@ -202,6 +208,8 @@ const PostFormFields = ({ onPublishReply, onPublishPost, handleUpload, + uploadFile, + onOekakiClearUploadedUrl, disableReplyPublish, }: PostFormFieldsProps) => ( <> @@ -365,6 +373,14 @@ const PostFormFields = ({ )} + {showOekakiControls && ( + + Draw + + + + + )} {((isInPostView && showSpoilerForReply) || (!isInPostView && showSpoilerForPost)) && ( {capitalize(t('spoiler'))} @@ -424,6 +440,7 @@ const PostFormFields = ({ }} /> + {showOekakiControls && isWebRuntime() ?
  • {OEKAKI_WEB_WARNING_TEXT}
  • : null} @@ -455,6 +472,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: const nonokoRedirectPathRef = useRef(null); const location = useLocation(); + const isInPostView = isPostPageView(location.pathname, params); const isInAllView = isAllView(location.pathname); const isInModView = isModView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); @@ -466,6 +484,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true; const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true; const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname); + const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i'; const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia; const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView)); const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry); @@ -600,7 +619,6 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: }, [postIndex, pendingPostBoardPath, resetFields, resetPublishPostOptions, navigate]); // in post page, publish a reply to the post - const isInPostView = isPostPageView(location.pathname, params); const cid = params?.commentCid || ''; const { isResolvingExternalQuotes, publishReply, publishReplyError, publishReplyStateMessage, resetPublishReplyOptions, replyIndex, setPublishReplyOptions } = usePublishReply({ cid, communityAddress, postCid }); @@ -707,7 +725,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: } }, [replyIndex, closeForm, navigate, resetFields]); - const { isUploading, uploadedFileName, handleUpload } = useFileUpload({ + const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({ onUploadComplete: (uploadedUrl: string) => { if (uploadedUrl) { setUrl(uploadedUrl); @@ -722,6 +740,21 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: } }, }); + const handleOekakiClearUploadedUrl = useCallback( + (uploadedUrl: string) => { + if ((urlRef.current?.value || url) !== uploadedUrl) return; + setUrl(''); + if (urlRef.current) { + urlRef.current.value = ''; + } + if (isInPostView) { + setPublishReplyOptions({ link: '' }); + } else { + setPublishPostOptions({ link: '' }); + } + }, + [isInPostView, setPublishPostOptions, setPublishReplyOptions, url], + ); const uploadMode = useMediaHostingStore((state) => state.uploadMode); const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime()); @@ -765,6 +798,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: isUploading={isUploading} uploadedFileName={uploadedFileName} showUploadControls={showUploadControls} + showOekakiControls={showOekakiControls} showSpoilerForPost={showSpoilerForPost} showSpoilerForReply={showSpoilerForReply} isInAllView={isInAllView} @@ -782,6 +816,8 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: onPublishReply={onPublishReply} onPublishPost={onPublishPost} handleUpload={handleUpload} + uploadFile={uploadFile} + onOekakiClearUploadedUrl={handleOekakiClearUploadedUrl} disableReplyPublish={isResolvingExternalQuotes} /> diff --git a/src/components/reply-modal/__tests__/reply-modal.test.tsx b/src/components/reply-modal/__tests__/reply-modal.test.tsx index cf4a2e0e..a31c310c 100644 --- a/src/components/reply-modal/__tests__/reply-modal.test.tsx +++ b/src/components/reply-modal/__tests__/reply-modal.test.tsx @@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client'; import { MemoryRouter } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import ReplyModal from '../reply-modal'; +import { OEKAKI_WEB_WARNING_TEXT } from '../../../lib/oekaki/oekaki-copy'; import { POST_OPTIONS_VALIDATION_DELAY_MS } from '../../../lib/utils/post-options-utils'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -20,7 +21,9 @@ const testState = vi.hoisted(() => ({ }, } as Record; title?: string }>, handleUploadMock: vi.fn(), + uploadFileMock: vi.fn(), isMobile: false, + mediaHostingRuntime: 'web' as 'web' | 'android' | 'electron', isResolvingExternalQuotes: false, isUploading: false, navigateMock: vi.fn(), @@ -127,8 +130,9 @@ vi.mock('../../../stores/use-reply-modal-store', () => ({ })); vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({ + getMediaHostingRuntime: () => testState.mediaHostingRuntime, getShowUploadControls: () => testState.showUploadControls, - isWebRuntime: () => true, + isWebRuntime: () => testState.mediaHostingRuntime === 'web', })); vi.mock('../../../stores/use-media-hosting-store', () => ({ @@ -185,6 +189,7 @@ vi.mock('../../../hooks/use-file-upload', () => ({ testState.uploadComplete = onUploadComplete; return { handleUpload: testState.handleUploadMock, + uploadFile: testState.uploadFileMock, isUploading: testState.isUploading, uploadedFileName: testState.uploadedFileName, }; @@ -352,6 +357,7 @@ describe('ReplyModal', () => { }, }; testState.handleUploadMock.mockReset(); + testState.uploadFileMock.mockReset(); testState.isMobile = false; testState.isResolvingExternalQuotes = false; testState.isUploading = false; @@ -398,6 +404,7 @@ describe('ReplyModal', () => { testState.uploadComplete = undefined; testState.uploadedFileName = null; testState.uploadMode = 'always'; + testState.mediaHostingRuntime = 'web'; container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); @@ -437,6 +444,30 @@ describe('ReplyModal', () => { expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' }); }); + it('shows Oekaki draw controls on /i/ replies', async () => { + testState.directoryByAddress['oekaki-posting.bso'] = { + address: 'oekaki-posting.bso', + directoryCode: 'i', + features: { requirePostLink: true, requirePostLinkIsMedia: true }, + title: '/i/ - Oekaki', + }; + testState.communities['oekaki-posting.bso'] = { address: 'oekaki-posting.bso' }; + + await renderReplyModal('/i/thread/post-1', 'oekaki-posting.bso'); + + expect(container.textContent).toContain('Size'); + expect(container.textContent).toContain('Replay'); + expect(Array.from(container.querySelectorAll('span')).some((span) => span.textContent === '×')).toBe(true); + expect(container.textContent).toContain(OEKAKI_WEB_WARNING_TEXT); + expect(Array.from(container.querySelectorAll('button')).some((button) => button.textContent === 'Draw')).toBe(true); + expect((Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Clear') as HTMLButtonElement | undefined)?.disabled).toBe(true); + + testState.mediaHostingRuntime = 'android'; + await renderReplyModal('/i/thread/post-1', 'oekaki-posting.bso'); + + expect(container.textContent).not.toContain(OEKAKI_WEB_WARNING_TEXT); + }); + it('shows a flag selector on flag boards and publishes the default geographic request', async () => { await renderReplyModal('/pol/thread/post-1', 'politically-incorrect.bso'); diff --git a/src/components/reply-modal/reply-modal.module.css b/src/components/reply-modal/reply-modal.module.css index 2b3fec0d..a3d3e56e 100644 --- a/src/components/reply-modal/reply-modal.module.css +++ b/src/components/reply-modal/reply-modal.module.css @@ -120,6 +120,32 @@ width: 130px; } +.oekakiRow { + display: flex; + align-items: flex-start; + width: 302px; + margin-bottom: 1px; + font-size: 10pt; +} + +.oekakiLabel { + display: inline-block; + width: 38px; + padding-top: 3px; + font-weight: bold; +} + +.oekakiControls { + width: 264px; +} + +.oekakiWarning { + width: 294px; + margin-bottom: 1px; + font-size: 11px; + line-height: 1.2; +} + .offlineBoard { width: 292px; font-family: monospace; diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index fea74a77..cd867dd1 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -28,9 +28,11 @@ import usePublishReply from '../../hooks/use-publish-reply'; import useIsMobile from '../../hooks/use-is-mobile'; import { useFileUpload } from '../../hooks/use-file-upload'; import { useCommunityField } from '../../hooks/use-stable-community'; +import { OEKAKI_WEB_WARNING_TEXT } from '../../lib/oekaki/oekaki-copy'; import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar'; import BoardOfflineAlert from '../board-offline-alert/board-offline-alert'; import LoadingEllipsis from '../loading-ellipsis'; +import OekakiDrawingControls from '../oekaki-drawing-controls'; import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message'; import styles from './reply-modal.module.css'; import capitalize from 'lodash/capitalize'; @@ -63,6 +65,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa const directoryEntry = findDirectoryByAddress(directories, communityAddress); const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true; const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname); + const showOekakiControls = postOptionsDirectoryCode === 'i' || directoryEntry?.directoryCode === 'i'; const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia; const requirePostLinkIsMedia = requirePostLinkIsMediaFeature === true || (requirePostLinkIsMediaFeature === undefined && (isInAllView || isInSubscriptionsView)); const flagOptions = getCommentFlagOptionsForDirectory(directoryEntry); @@ -429,7 +432,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa checkContentLengthRef.current(publishContent, t); }, [showReplyModal, quoteInsertRequestId, quoteInsertNumber, quoteInsertSelectedText, postOptionsDirectoryCode, setPublishReplyOptions, t]); - const { isUploading, uploadedFileName, handleUpload } = useFileUpload({ + const { isUploading, uploadedFileName, handleUpload, uploadFile } = useFileUpload({ onUploadComplete: (uploadedUrl: string) => { if (uploadedUrl) { setUrl(uploadedUrl); @@ -440,6 +443,14 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa } }, }); + const handleOekakiClearUploadedUrl = (uploadedUrl: string) => { + if ((urlRef.current?.value || url) !== uploadedUrl) return; + setUrl(''); + if (urlRef.current) { + urlRef.current.value = ''; + } + setPublishReplyOptions({ link: '' }); + }; const uploadMode = useMediaHostingStore((state) => state.uploadMode); const showUploadControls = getShowUploadControls(uploadMode, isWebRuntime()); const displayedFileName = getPublishURLFilename(url) || uploadedFileName; @@ -546,6 +557,13 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa }} /> + {showOekakiControls && ( +
    + Draw + +
    + )} + {showOekakiControls && isWebRuntime() ?
    {OEKAKI_WEB_WARNING_TEXT}
    : null} {flagOptions.length > 0 && (