diff --git a/docs/generate_mockup.md b/docs/generate_mockup.md new file mode 100644 index 0000000..c85ab6f --- /dev/null +++ b/docs/generate_mockup.md @@ -0,0 +1,19 @@ +## Why add padding in `create_fit_coord_image` + +Because there will see weird triangle in the corner for some deivces because there is a black area between the phone frame and the display area + +![samsung-s24-ultra-right](https://github.com/user-attachments/assets/75936a3a-89b9-4885-874f-dfac4a0d1d0d) + +| type | Before | After | +| --------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| landscape | ![samsung-s24-ultra-landscape](https://github.com/user-attachments/assets/b3ec7528-20a7-4508-b8c2-f7b6308e59c8) | ![samsung-s24-ultra-landscape](https://github.com/user-attachments/assets/bb508211-edd4-4dc3-b5d1-ed228150c12b) | +| portrait | ![samsung-s24-ultra-portrait](https://github.com/user-attachments/assets/a9f56fec-6524-4983-9d21-2c598e1094a5) | ![samsung-s24-ultra-portrait](https://github.com/user-attachments/assets/96c6046e-be8e-4b36-8a2f-b374ffbe7d79) | +| left | ![samsung-s24-ultra-left](https://github.com/user-attachments/assets/5e90d39b-40ff-44b8-96c1-e8479a17a9f5) | ![samsung-s24-ultra-left](https://github.com/user-attachments/assets/e33e5a43-69d9-494c-acae-dc371abe7406) | +| right | ![samsung-s24-ultra-right](https://github.com/user-attachments/assets/8d258f18-bfd9-47a9-8258-3285f1a01b8a) | ![samsung-s24-ultra-right](https://github.com/user-attachments/assets/15768da2-e0af-442e-9c62-f99510c485e4) | + +For the portrait one, the gray area on the top disappear after the update, but I think that should be expected, I guess the `coords` in `device_info.json` is incorrect so the image is pasted slightly to the bottom than expected , but it is not related to the current issue. (not sure if we need to spend some time checking that all model templates and configurations are correct) + +## why shift mask position in `create_mockup_image` + +After adding padding in `create_fit_coord_image`, we need to shift mask too get correct mockup +![image](https://github.com/user-attachments/assets/c751f8a2-4ff5-4a53-8eff-d58432b2f48a) diff --git a/mockup_package/mockup/__init__.py b/mockup_package/mockup/__init__.py index 66c9d18..e291fc3 100644 --- a/mockup_package/mockup/__init__.py +++ b/mockup_package/mockup/__init__.py @@ -1 +1,2 @@ from .image import mockup as startMockup # noqa: F401 +from .image import previewMockup # noqa: F401 diff --git a/mockup_package/mockup/image.py b/mockup_package/mockup/image.py index 3b788d7..ca68de7 100755 --- a/mockup_package/mockup/image.py +++ b/mockup_package/mockup/image.py @@ -1,49 +1,84 @@ import base64 import sys from pathlib import Path +from typing import Any from pyodide.http import pyfetch from mockup.image_generator import ImageGenerator as IG import os -async def mockup(location, device_id, original_img_path_list, device_info): - device_path_prefix = f"{location.split('/')[0]}//{location.split('/')[2]}" - device_mask_path_prefix = device_path_prefix + "/images/mockup_mask_templates/" +async def generate( + location: str, + original_img_path: str, + spec: dict[str, Any], + ig: IG, +) -> tuple[str, str, str]: + device_path_prefix: str = f"{location.split('/')[0]}//{location.split('/')[2]}" + device_mask_path_prefix: str = device_path_prefix + "/images/mockup_mask_templates/" device_path_prefix += "/images/mockup_templates/" - device_path = "./device.png" - device_mask_path = "./device_mask.png" - output_img_path_list = [] + device_path: str = "./device.png" + device_mask_path: str = "./device_mask.png" + + try: + await process_response( + device_path_prefix + str(spec["image"]), + device_path, + ) + await process_response( + device_mask_path_prefix + str(spec["image"]), + device_mask_path, + ) + except Exception as e: + print(e, file=sys.stderr) + # js.errorBox(e) + raise + ig.create_fit_coord_image(spec) + deviceView = str(spec["image"]).split("-")[-1].split(".")[0] + path = ( + f"{os.path.splitext(os.path.basename(original_img_path))[0]}" + + f"-{deviceView}.png" + ) + ig.create_mockup_image(device_path, device_mask_path, path) + return (path, original_img_path, deviceView) + + +async def mockup( + location: str, + device_id: str, + original_img_path_list: list[str], + device_info: dict[str, Any], +): + output_img_path_list: list[tuple[str, str, str]] = [] for original_img_path in original_img_path_list: ig = IG(original_img_path, device_id, device_info) ig.create_fit_resolution_image() for spec in ig.phone_models.get(device_id).get("mockups").values(): - try: - await process_response( - device_path_prefix + str(spec["image"]), - device_path, - ) - await process_response( - device_mask_path_prefix + str(spec["image"]), - device_mask_path, - ) - except Exception as e: - print(e, file=sys.stderr) - # js.errorBox(e) - raise - ig.create_fit_coord_image(spec) - deviceView = str(spec["image"]).split("-")[-1].split(".")[0] - path = ( - f"{os.path.splitext(os.path.basename(original_img_path))[0]}" - + f"-{deviceView}.png" + output_img_path_list.append( + await generate(location, original_img_path, spec, ig) ) - ig.create_mockup_image(device_path, device_mask_path, path) - output_img_path_list.append([path, original_img_path, deviceView]) original_img_path_list.clear() return output_img_path_list -async def download(url): +async def previewMockup( + location: str, + device_id: str, + original_img_path: str, + device_info: dict[str, Any], + preview_orientation_index: int = 0, +): + ig = IG(original_img_path, device_id, device_info) + ig.create_fit_resolution_image() + spec = list(ig.phone_models.get(device_id).get("mockups").values())[ + preview_orientation_index + ] + output_img_path = await generate(location, original_img_path, spec, ig) + + return output_img_path + + +async def download(url: str): filename = Path(url).name response = await pyfetch(url) if response.status == 200: @@ -57,7 +92,7 @@ async def download(url): return filename, status -async def process_response(url, path): +async def process_response(url: str, path: str): response_content = await download(url) if response_content[1] == 200: data = base64.b64encode(open(response_content[0], "rb").read()) diff --git a/mockup_package/mockup/image_generator.py b/mockup_package/mockup/image_generator.py index d293e26..425d5d7 100755 --- a/mockup_package/mockup/image_generator.py +++ b/mockup_package/mockup/image_generator.py @@ -1,6 +1,6 @@ import os import mockup.helpers as h -from PIL import Image +from PIL import Image, ExifTags import cv2 import numpy as np @@ -38,6 +38,31 @@ def __init__(self, original_img_path, phone_slug, device_info): def create_fit_resolution_image(self): device_ratio = self.d_size[0] / self.d_size[1] image = Image.open(self.original_img_path).convert("RGBA") + + # Solve image orientation issue: + # some picture has orientation property, which the browser knows and can automatically rotate the image # noqa: E501 + # but the property is lost when we mockup the image, so need to fix it manually. # noqa: E501 + # ref: https://stackoverflow.com/questions/13872331/rotating-an-image-with-orientation-specified-in-exif-using-python-without-pil-in # noqa: E501 + try: + for orientation in ExifTags.TAGS.keys(): + if ExifTags.TAGS[orientation] == "Orientation": + break + + exif = image.getexif() + if exif[orientation] == 3: + image = image.rotate(180, expand=True) + elif exif[orientation] == 6: + image = image.rotate(270, expand=True) + elif exif[orientation] == 8: + image = image.rotate(90, expand=True) + except KeyError as ex: + if ex.args == (274,): + # image does not have orientation, expected case, will silent error + # ref https://github.com/python-pillow/Pillow/blob/0ec1153a627a46b978022c68c2adce89ff81f40d/src/PIL/TiffTags.py#L145 + pass + else: + raise + find_original_image_dim_process = str(image.size[0]) + "x" + str(image.size[1]) original_image_dim = tuple( map( @@ -165,15 +190,16 @@ def create_mockup_image(self, device_path, device_mask_path_prefix, result_path) tmp = self.target_points[0].copy() self.target_points[0] = self.target_points[1] self.target_points[1] = tmp + shifted_points = self.target_points + np.array([PADDING, PADDING]) mask = np.zeros(basemap.shape[:2], np.uint8) cv2.polylines( mask, - pts=[self.target_points], + pts=[shifted_points], isClosed=True, color=(255, 0, 0), thickness=3, ) - cv2.fillPoly(mask, [self.target_points], 255) + cv2.fillPoly(mask, [shifted_points], 255) mask = cv2.cvtColor(mask, cv2.COLOR_BGR2RGBA) mask = Image.fromarray(mask) @@ -184,7 +210,9 @@ def create_mockup_image(self, device_path, device_mask_path_prefix, result_path) # If you want to use the mask directly with the first argument of the "paste" function, # noqa: E501 # you need to convert it to the "L" mode and ensure that it has the same size as the first argument. # noqa: E501 device_image.paste( - tmp_result_image, (self.xyset[0], self.xyset[2]), mask.convert("L") + tmp_result_image, + (self.xyset[0] - PADDING, self.xyset[2] - PADDING), + mask.convert("L"), ) device_image.save(result_path) diff --git a/public/image_process.py b/public/image_process.py index 20751d3..f3f6782 100644 --- a/public/image_process.py +++ b/public/image_process.py @@ -6,7 +6,9 @@ from PIL import Image -async def upload_single_image(origin_image, file_name, original_img_path_list): +async def upload_single_image_and_save_to_list( + origin_image, file_name, original_img_path_list +): array_buf = Uint8Array.new(await origin_image.arrayBuffer()) bytes_list = bytearray(array_buf) origin_bytes = io.BytesIO(bytes_list) @@ -16,17 +18,56 @@ async def upload_single_image(origin_image, file_name, original_img_path_list): my_image.save(filePath) +async def upload_single_image(origin_image, file_name): + array_buf = Uint8Array.new(await origin_image.arrayBuffer()) + bytes_list = bytearray(array_buf) + origin_bytes = io.BytesIO(bytes_list) + my_image = Image.open(origin_bytes) + filePath = f"./{file_name}.png" + my_image.save(filePath) + return filePath + + +async def upload_file(): + from js import imageUpload + + # Since we will update `imageUpload` when calling this function, + # need to re-import it to force update to new value + # or we will always generate first uploaded image + basename, ext = os.path.splitext(imageUpload.name) + if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: + return + original_img_path = await upload_single_image(imageUpload, basename) + return original_img_path + + async def upload_files(): original_img_path_list = [] for fileItem in imageUploadList: basename, ext = os.path.splitext(fileItem.name) if ext.lower() not in [".psd", ".jpg", ".jpeg", ".png"]: return - await upload_single_image(fileItem, basename, original_img_path_list) + await upload_single_image_and_save_to_list( + fileItem, basename, original_img_path_list + ) return original_img_path_list -def save_image(imageList): +def save_image(image): + print("image", image) + path = image[0] + my_image = Image.open(path) + my_stream = io.BytesIO() + my_image.save(my_stream, format="PNG") + binary_fc = open(path, "rb").read() + base64_utf8_str = base64.b64encode(binary_fc).decode("utf-8") + basename, ext = os.path.splitext(path) + dataurl = f"data:image/{ext};base64,{base64_utf8_str}" + print(basename) + return [f"img{basename}", dataurl] + + +def save_images(imageList): returnList = [] for image in imageList: path = image[0] diff --git a/public/images/devices_picture/apple-imac2013-front.png b/public/images/devices_picture/apple-imac2013-front.png index 91fa8b3..ee04737 100644 Binary files a/public/images/devices_picture/apple-imac2013-front.png and b/public/images/devices_picture/apple-imac2013-front.png differ diff --git a/public/images/devices_picture/apple-imac2013-left.png b/public/images/devices_picture/apple-imac2013-left.png index 437dab1..72bc2f6 100644 Binary files a/public/images/devices_picture/apple-imac2013-left.png and b/public/images/devices_picture/apple-imac2013-left.png differ diff --git a/public/images/devices_picture/apple-imac2013-right.png b/public/images/devices_picture/apple-imac2013-right.png index 0079ea3..da5e7aa 100644 Binary files a/public/images/devices_picture/apple-imac2013-right.png and b/public/images/devices_picture/apple-imac2013-right.png differ diff --git a/public/images/devices_picture/apple-imac2015-front.png b/public/images/devices_picture/apple-imac2015-front.png index 3656a86..646affc 100644 Binary files a/public/images/devices_picture/apple-imac2015-front.png and b/public/images/devices_picture/apple-imac2015-front.png differ diff --git a/public/images/devices_picture/apple-imac2015retina-front.png b/public/images/devices_picture/apple-imac2015retina-front.png index 90946e3..77134ff 100644 Binary files a/public/images/devices_picture/apple-imac2015retina-front.png and b/public/images/devices_picture/apple-imac2015retina-front.png differ diff --git a/public/images/devices_picture/apple-macbook-gold-front.png b/public/images/devices_picture/apple-macbook-gold-front.png index 15c3f5f..17b3de9 100644 Binary files a/public/images/devices_picture/apple-macbook-gold-front.png and b/public/images/devices_picture/apple-macbook-gold-front.png differ diff --git a/public/images/devices_picture/apple-macbook-grey-front.png b/public/images/devices_picture/apple-macbook-grey-front.png index 9504c8c..cf7afde 100644 Binary files a/public/images/devices_picture/apple-macbook-grey-front.png and b/public/images/devices_picture/apple-macbook-grey-front.png differ diff --git a/public/images/devices_picture/google-pixel-quite-black-landscape.png b/public/images/devices_picture/google-pixel-quite-black-landscape.png index 4dba60f..20c1c6d 100644 Binary files a/public/images/devices_picture/google-pixel-quite-black-landscape.png and b/public/images/devices_picture/google-pixel-quite-black-landscape.png differ diff --git a/public/images/devices_picture/google-pixel-quite-black-portrait.png b/public/images/devices_picture/google-pixel-quite-black-portrait.png index 682a4e5..d48e4fd 100644 Binary files a/public/images/devices_picture/google-pixel-quite-black-portrait.png and b/public/images/devices_picture/google-pixel-quite-black-portrait.png differ diff --git a/public/images/devices_picture/google-pixel-really-blue-landscape.png b/public/images/devices_picture/google-pixel-really-blue-landscape.png index cbd7c28..9bd82e1 100644 Binary files a/public/images/devices_picture/google-pixel-really-blue-landscape.png and b/public/images/devices_picture/google-pixel-really-blue-landscape.png differ diff --git a/public/images/devices_picture/google-pixel-really-blue-portrait.png b/public/images/devices_picture/google-pixel-really-blue-portrait.png index 66fce57..0022a5a 100644 Binary files a/public/images/devices_picture/google-pixel-really-blue-portrait.png and b/public/images/devices_picture/google-pixel-really-blue-portrait.png differ diff --git a/public/images/devices_picture/google-pixel-very-silver-landscape.png b/public/images/devices_picture/google-pixel-very-silver-landscape.png index 87f3d8f..c434367 100644 Binary files a/public/images/devices_picture/google-pixel-very-silver-landscape.png and b/public/images/devices_picture/google-pixel-very-silver-landscape.png differ diff --git a/public/images/devices_picture/google-pixel-very-silver-portrait.png b/public/images/devices_picture/google-pixel-very-silver-portrait.png index 3bd2c6f..f5bd66b 100644 Binary files a/public/images/devices_picture/google-pixel-very-silver-portrait.png and b/public/images/devices_picture/google-pixel-very-silver-portrait.png differ diff --git a/public/images/devices_picture/lg-55lw5600-front.png b/public/images/devices_picture/lg-55lw5600-front.png index 71c3403..63301ff 100644 Binary files a/public/images/devices_picture/lg-55lw5600-front.png and b/public/images/devices_picture/lg-55lw5600-front.png differ diff --git a/public/images/devices_picture/lg-55lw5600-side.png b/public/images/devices_picture/lg-55lw5600-side.png index 43b54d2..ef3a81c 100644 Binary files a/public/images/devices_picture/lg-55lw5600-side.png and b/public/images/devices_picture/lg-55lw5600-side.png differ diff --git a/public/images/devices_picture/lg-tm2792-front.png b/public/images/devices_picture/lg-tm2792-front.png index cee6f0e..aaa3187 100644 Binary files a/public/images/devices_picture/lg-tm2792-front.png and b/public/images/devices_picture/lg-tm2792-front.png differ diff --git a/public/images/devices_picture/lg-tm2792-side.png b/public/images/devices_picture/lg-tm2792-side.png index 9c54bd7..80e52e3 100644 Binary files a/public/images/devices_picture/lg-tm2792-side.png and b/public/images/devices_picture/lg-tm2792-side.png differ diff --git a/public/images/devices_picture/samsung-d8000-front.png b/public/images/devices_picture/samsung-d8000-front.png index f543887..4441b77 100644 Binary files a/public/images/devices_picture/samsung-d8000-front.png and b/public/images/devices_picture/samsung-d8000-front.png differ diff --git a/public/images/devices_picture/samsung-d8000-side.png b/public/images/devices_picture/samsung-d8000-side.png index 9d546e5..4f18b94 100644 Binary files a/public/images/devices_picture/samsung-d8000-side.png and b/public/images/devices_picture/samsung-d8000-side.png differ diff --git a/public/images/devices_picture/samsung-es8000-front.png b/public/images/devices_picture/samsung-es8000-front.png index 5709a03..ad58683 100644 Binary files a/public/images/devices_picture/samsung-es8000-front.png and b/public/images/devices_picture/samsung-es8000-front.png differ diff --git a/public/images/devices_picture/samsung-es8000-side.png b/public/images/devices_picture/samsung-es8000-side.png index 119831f..3ada11d 100644 Binary files a/public/images/devices_picture/samsung-es8000-side.png and b/public/images/devices_picture/samsung-es8000-side.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-black-landscape.png b/public/images/devices_picture/samsung-galaxy-watch-4-black-landscape.png new file mode 100644 index 0000000..90491ce Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-black-landscape.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-black-portrait.png b/public/images/devices_picture/samsung-galaxy-watch-4-black-portrait.png new file mode 100644 index 0000000..3ffb7e2 Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-black-portrait.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-landscape.png b/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-landscape.png new file mode 100644 index 0000000..1a2c9eb Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-landscape.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-portrait.png b/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-portrait.png new file mode 100644 index 0000000..36f5902 Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-pink-gold-portrait.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-silver-landscape.png b/public/images/devices_picture/samsung-galaxy-watch-4-silver-landscape.png new file mode 100644 index 0000000..36091c2 Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-silver-landscape.png differ diff --git a/public/images/devices_picture/samsung-galaxy-watch-4-silver-portrait.png b/public/images/devices_picture/samsung-galaxy-watch-4-silver-portrait.png new file mode 100644 index 0000000..b61ab09 Binary files /dev/null and b/public/images/devices_picture/samsung-galaxy-watch-4-silver-portrait.png differ diff --git a/public/images/devices_picture/samsung-galaxys4-black-landscape.png b/public/images/devices_picture/samsung-galaxys4-black-landscape.png index 8aa01bd..6ac6efc 100644 Binary files a/public/images/devices_picture/samsung-galaxys4-black-landscape.png and b/public/images/devices_picture/samsung-galaxys4-black-landscape.png differ diff --git a/public/images/devices_picture/samsung-galaxys4-black-portrait.png b/public/images/devices_picture/samsung-galaxys4-black-portrait.png index 5bf4e30..3d27d8b 100644 Binary files a/public/images/devices_picture/samsung-galaxys4-black-portrait.png and b/public/images/devices_picture/samsung-galaxys4-black-portrait.png differ diff --git a/public/images/devices_picture/samsung-galaxys4-white-landscape.png b/public/images/devices_picture/samsung-galaxys4-white-landscape.png index be34d5b..2b22033 100644 Binary files a/public/images/devices_picture/samsung-galaxys4-white-landscape.png and b/public/images/devices_picture/samsung-galaxys4-white-landscape.png differ diff --git a/public/images/devices_picture/samsung-galaxys4-white-portrait.png b/public/images/devices_picture/samsung-galaxys4-white-portrait.png index fd341c7..af83321 100644 Binary files a/public/images/devices_picture/samsung-galaxys4-white-portrait.png and b/public/images/devices_picture/samsung-galaxys4-white-portrait.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-landscape.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-landscape.png new file mode 100644 index 0000000..3bb47e0 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-landscape.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-portrait.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-portrait.png new file mode 100644 index 0000000..4cab425 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-black-portrait.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-landscape.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-landscape.png new file mode 100644 index 0000000..f7c6a13 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-landscape.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-portrait.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-portrait.png new file mode 100644 index 0000000..8922954 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-pink-gold-portrait.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-landscape.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-landscape.png new file mode 100644 index 0000000..9cb9821 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-landscape.png differ diff --git a/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-portrait.png b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-portrait.png new file mode 100644 index 0000000..272e662 Binary files /dev/null and b/public/images/mockup_mask_templates/samsung-galaxy-watch-4-silver-portrait.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-black-landscape.png b/public/images/mockup_templates/samsung-galaxy-watch-4-black-landscape.png new file mode 100644 index 0000000..a015b4f Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-black-landscape.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-black-portrait.png b/public/images/mockup_templates/samsung-galaxy-watch-4-black-portrait.png new file mode 100644 index 0000000..362becc Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-black-portrait.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-landscape.png b/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-landscape.png new file mode 100644 index 0000000..9b76013 Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-landscape.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-portrait.png b/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-portrait.png new file mode 100644 index 0000000..7d4e3a2 Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-pink-gold-portrait.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-silver-landscape.png b/public/images/mockup_templates/samsung-galaxy-watch-4-silver-landscape.png new file mode 100644 index 0000000..525e573 Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-silver-landscape.png differ diff --git a/public/images/mockup_templates/samsung-galaxy-watch-4-silver-portrait.png b/public/images/mockup_templates/samsung-galaxy-watch-4-silver-portrait.png new file mode 100644 index 0000000..ab47276 Binary files /dev/null and b/public/images/mockup_templates/samsung-galaxy-watch-4-silver-portrait.png differ diff --git a/public/images/preview-gray.svg b/public/images/preview-gray.svg new file mode 100644 index 0000000..673dfa4 --- /dev/null +++ b/public/images/preview-gray.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/preview-white.svg b/public/images/preview-white.svg new file mode 100644 index 0000000..b857f64 --- /dev/null +++ b/public/images/preview-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/spinner-blue-2.png b/public/images/spinner-blue-2.png new file mode 100644 index 0000000..0ee2e96 Binary files /dev/null and b/public/images/spinner-blue-2.png differ diff --git a/public/mockup.zip b/public/mockup.zip index d78ad2d..c3fc509 100644 Binary files a/public/mockup.zip and b/public/mockup.zip differ diff --git a/public/scripts/menu.js b/public/scripts/menu.js deleted file mode 100644 index a2274d1..0000000 --- a/public/scripts/menu.js +++ /dev/null @@ -1,3 +0,0 @@ -document.querySelector(".hamburger").addEventListener("click", () => { - document.querySelector(".nav-links").classList.toggle("expanded"); -}); diff --git a/public/scripts/models/image-upload.js b/public/scripts/models/image-upload.js index 513ef0c..7b7afb9 100644 --- a/public/scripts/models/image-upload.js +++ b/public/scripts/models/image-upload.js @@ -2,13 +2,16 @@ Require: mobx, psd.js */ -const ReadState = { +const ImageUploadState = { ReadyForRead: "ReadyForRead", Reading: "Reading", + ReadyForPreview: "ReadyForPreview", + GeneratingPreview: "GeneratingPreviewImage", ReadSuccess: "ReadSuccess", ErrUnsupportedFileType: "ErrUnsupportedFileType", ErrExceedMaxFileSize: "ErrExceedMaxFileSize", ErrRead: "ErrRead", + ErrPreview: "ErrPreview", }; class ImageUpload { @@ -18,8 +21,10 @@ class ImageUpload { height = null; uuid = null; signedData = null; - readState = ReadState.ReadyForRead; + state = ImageUploadState.ReadyForRead; message = null; + ulid = null; + previewUrl = null; loadDimensionPromise = null; @@ -30,7 +35,7 @@ class ImageUpload { height: mobx.observable, uuid: mobx.observable, signedData: mobx.observable, - readState: mobx.observable, + state: mobx.observable, message: mobx.observable, isProcessingState: mobx.computed, isSuccessState: mobx.computed, @@ -42,21 +47,29 @@ class ImageUpload { } async read() { - this.readState = ReadState.Reading; + this.state = ImageUploadState.Reading; if (!(await this._verifyFileType())) { - this.readState = ReadState.ErrUnsupportedFileType; + this.state = ImageUploadState.ErrUnsupportedFileType; return; } if (!this._verifyFileSize()) { - this.readState = ReadState.ErrExceedMaxFileSize; + this.state = ImageUploadState.ErrExceedMaxFileSize; return; } const loadDimensionResult = await this._loadDimension(); if (loadDimensionResult.type === "failed") { - this.readState = loadDimensionResult.reason; + this.state = loadDimensionResult.reason; return; } - this.readState = ReadState.ReadSuccess; + this.state = ImageUploadState.ReadyForPreview; + } + + updateState(state) { + this.state = state; + } + + updatePreviewUrl(previewUrl) { + this.previewUrl = previewUrl; } // Cache file type from header such that no need to parse again @@ -132,11 +145,11 @@ class ImageUpload { }; fileReader.onabort = () => { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); @@ -157,39 +170,52 @@ class ImageUpload { }; img.onerror = () => { console.warn("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.onabort = () => { console.warn("onabort"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; img.src = fileReader.result; }; fileReader.onabort = () => { - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.onerror = () => { console.log("onerror"); - resolve({ type: "failed", reason: ReadState.ErrRead }); + resolve({ type: "failed", reason: ImageUploadState.ErrRead }); }; fileReader.readAsDataURL(this.file); }); } + get isGeneratingPreviewState() { + return this.state === ImageUploadState.GeneratingPreview; + } + get isProcessingState() { return !this.isProcessedState; } get isSuccessState() { - return this.readState === ReadState.ReadSuccess; + return this.state === ImageUploadState.ReadSuccess; + } + + get isReadyForPreview() { + return this.state === ImageUploadState.ReadyForPreview; } get isProcessedState() { - return this.isErrorState || this.isSuccessState; + return ( + this.isErrorState || + this.isSuccessState || + this.isGeneratingPreviewState || + this.isReadyForPreview + ); } get isErrorState() { - return this.readState.startsWith("Err"); + return this.state.startsWith("Err"); } get imageFile() { diff --git a/public/scripts/preview_worker.js b/public/scripts/preview_worker.js new file mode 100644 index 0000000..9c9cc77 --- /dev/null +++ b/public/scripts/preview_worker.js @@ -0,0 +1,69 @@ +importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"); + +async function initianPyodide() { + console.log("start startup"); + const pyodide = await loadPyodide(); + await pyodide.loadPackage(["numpy", "opencv-python", "pillow", "micropip"]); + let zipResponse = await fetch("/mockup.zip"); + let zipBinary = await zipResponse.arrayBuffer(); + pyodide.unpackArchive(zipBinary, "zip"); + await pyodide.runPythonAsync( + ` + from pyodide.http import pyfetch + response = await pyfetch("/image_process.py") + with open("./image_process.py", "wb") as f: + f.write(await response.bytes()) + `, + (output) => console.log(output), + (output) => console.log(output), + ); + console.log("end up"); + return pyodide; +} + +// Now only the first orientation model is generated for preview +async function runPreviewMockup(pyodide) { + let pythonNamespace = pyodide.globals.get("dict")(); + await pyodide.runPythonAsync( + ` + import mockup + import image_process + from js import locationKey, imageUpload, deviceInfo, deviceId + origin_image_path = await image_process.upload_file() + print("start preview", origin_image_path) + output_img = await mockup.previewMockup(locationKey, deviceId, origin_image_path, deviceInfo) + `, + { globals: pythonNamespace }, + ); + pyodide.runPython( + ` + temp = image_process.save_image(output_img) + `, + { globals: pythonNamespace }, + ); + return pythonNamespace.get("temp").toJs(); +} + +async function main() { + let pyodideObject = initianPyodide(); + self.onmessage = async (event) => { + pyodideObject = await pyodideObject; + + self["imageUploadList"] = undefined; + self["imageUpload"] = event.data.imageUpload; + self["locationKey"] = event.data.location; + self["deviceId"] = event.data.deviceId; + self["deviceInfo"] = event.data.deviceInfo; + + try { + // TODO: Handle preview loading state in widget + let results = await runPreviewMockup(pyodideObject); + console.log("preview results", results); + self.postMessage({ ulid: event.data.ulid, results: results }); + } catch (error) { + self.postMessage({ ulid: event.data.ulid, error: error.message }); + } + }; +} + +main(); diff --git a/public/scripts/ulid.min.js b/public/scripts/ulid.min.js new file mode 100644 index 0000000..8c1fb9d --- /dev/null +++ b/public/scripts/ulid.min.js @@ -0,0 +1,130 @@ +/** + * Minified by jsDelivr using Terser v5.19.2. + * Original file: /npm/ulid@2.3.0/dist/index.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!(function (r, e) { + "object" == typeof exports && "undefined" != typeof module + ? e(exports) + : "function" == typeof define && define.amd + ? define(["exports"], e) + : e((r.ULID = {})); +})(this, function (r) { + "use strict"; + function e(r) { + var e = new Error(r); + return (e.source = "ulid"), e; + } + var t = "0123456789ABCDEFGHJKMNPQRSTVWXYZ", + n = t.length, + o = Math.pow(2, 48) - 1; + function i(r, e, t) { + return e > r.length - 1 ? r : r.substr(0, e) + t + r.substr(e + 1); + } + function u(r) { + for ( + var o = void 0, u = r.length, a = void 0, c = void 0, f = n - 1; + !o && u-- >= 0; + + ) { + if (((a = r[u]), -1 === (c = t.indexOf(a)))) + throw e("incorrectly encoded string"); + c !== f ? (o = i(r, u, t[c + 1])) : (r = i(r, u, t[0])); + } + if ("string" == typeof o) return o; + throw e("cannot increment this string"); + } + function a(r) { + var e = Math.floor(r() * n); + return e === n && (e = n - 1), t.charAt(e); + } + function c(r, i) { + if (isNaN(r)) throw new Error(r + " must be a number"); + if (r > o) throw e("cannot encode time greater than " + o); + if (r < 0) throw e("time must be positive"); + if (!1 === Number.isInteger(r)) throw e("time must be an integer"); + for (var u = void 0, a = ""; i > 0; i--) + (u = r % n), (a = t.charAt(u) + a), (r = (r - u) / n); + return a; + } + function f(r, e) { + for (var t = ""; r > 0; r--) t = a(e) + t; + return t; + } + function d() { + var r = arguments.length > 0 && void 0 !== arguments[0] && arguments[0], + t = arguments[1]; + t || (t = "undefined" != typeof window ? window : null); + var n = t && (t.crypto || t.msCrypto); + if (n) + return function () { + var r = new Uint8Array(1); + return n.getRandomValues(r), r[0] / 255; + }; + try { + var o = require("crypto"); + return function () { + return o.randomBytes(1).readUInt8() / 255; + }; + } catch (r) {} + if (r) { + try { + console.error( + "secure crypto unusable, falling back to insecure Math.random()!", + ); + } catch (r) {} + return function () { + return Math.random(); + }; + } + throw e("secure crypto unusable, insecure Math.random not allowed"); + } + function s(r) { + return ( + r || (r = d()), + function (e) { + return isNaN(e) && (e = Date.now()), c(e, 10) + f(16, r); + } + ); + } + var h = s(); + (r.replaceCharAt = i), + (r.incrementBase32 = u), + (r.randomChar = a), + (r.encodeTime = c), + (r.encodeRandom = f), + (r.decodeTime = function (r) { + if (26 !== r.length) throw e("malformed ulid"); + var i = r + .substr(0, 10) + .split("") + .reverse() + .reduce(function (r, o, i) { + var u = t.indexOf(o); + if (-1 === u) throw e("invalid character found: " + o); + return r + u * Math.pow(n, i); + }, 0); + if (i > o) throw e("malformed ulid, timestamp too large"); + return i; + }), + (r.detectPrng = d), + (r.factory = s), + (r.monotonicFactory = function (r) { + r || (r = d()); + var e = 0, + t = void 0; + return function (n) { + if ((isNaN(n) && (n = Date.now()), n <= e)) { + var o = (t = u(t)); + return c(e, 10) + o; + } + e = n; + var i = (t = f(16, r)); + return c(n, 10) + i; + }; + }), + (r.ulid = h), + Object.defineProperty(r, "__esModule", { value: !0 }); +}); +//# sourceMappingURL=/sm/9461d47ebd40c139f3bc709023a78452e95dfaaa072b412c6fd663f1d02514a4.map diff --git a/public/scripts/upload.js b/public/scripts/upload.js index 7a1f263..bcc54e2 100644 --- a/public/scripts/upload.js +++ b/public/scripts/upload.js @@ -34,6 +34,18 @@ async function runWorker(worker) { window.localforage .setItem("pictureArray", e.data) .then(function (pictureArray) { + if (e.data["error"] !== undefined) { + console.log("Get error while generating mockup", e.data["error"]); + window.viewModel.cancelMockup(); + + // Alert after `cancelMockup` finish + setTimeout(() => { + alert( + "Oops, something went wrong. Please try a different image/device.\nIf it persists, we'd appreciate if you report it on our GitHub 🙏 https://github.com/oursky/mockuphone.com/issues.", + ); + }, 100); + return; + } window.location.href = "/download/?deviceId=" + window.workerDeviceId; }) .catch(function (err) { @@ -44,6 +56,75 @@ async function runWorker(worker) { ); } +function runPreviewWorker(worker, imageUpload) { + if (imageUpload.isErrorState) { + return; + } + window.viewModel.fileList.updateImageUploadStateByULID( + imageUpload.ulid, + ImageUploadState.GeneratingPreview, + ); + const imageUploadFile = imageUpload.file; + worker.postMessage({ + imageUpload: imageUploadFile, + location: window.location.toString(), + deviceId: window.workerDeviceId, + deviceInfo: window.deviceInfo, + ulid: imageUpload.ulid, + }); + worker.addEventListener( + "message", + function (e) { + if (e.data["error"] !== undefined) { + console.log( + "Get error while generating preview image", + e.data["error"], + ); + window.viewModel.fileList.updateImageUploadStateByULID( + e.data["ulid"], + ImageUploadState.ErrPreview, + ); + return; + } + + const ulid = e.data["ulid"]; + const [_, previewUrl] = e.data["results"]; + + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + + /* Put first generated mockup to preview area */ + if (window.viewModel.selectedPreviewImageULID === ulid) { + imageContainer.style.backgroundImage = `url(${previewUrl})`; + + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "none"; + }); + + // scroll to preview section on mobile devices + if (window.innerWidth <= 992) { + const previewSection = document.querySelector(".device"); + const HEADER_HEIGHT = 80; + scrollToElementTop(previewSection, HEADER_HEIGHT); + } + } + window.viewModel.fileList.updateImageUploadPreviewUrlByULID( + ulid, + previewUrl, + ); + window.viewModel.fileList.updateImageUploadStateByULID( + ulid, + ImageUploadState.ReadSuccess, + ); + }, + false, + ); +} + class FileListViewModel { maxFileSizeByte = null; _imageUploads = []; @@ -85,18 +166,55 @@ class FileListViewModel { files = [file]; } - for (const file of files) { - const imageUpload = new ImageUpload(file, MAX_FILE_SIZE_BYTE); + for (let i = 0; i < files.length; i += 1) { + const imageUpload = new ImageUpload(files[i], MAX_FILE_SIZE_BYTE); await imageUpload.read(); - this._imageUploads.push(imageUpload); + imageUpload.ulid = ULID.ulid(); + + if (window.viewModel.selectedPreviewImageULID === null && i === 0) { + window.viewModel.selectedPreviewImageULID = imageUpload.ulid; + + // scroll to file list section on mobile devices + if (window.innerWidth <= 992) { + const fileListSection = document.querySelector(".file-list"); + const HEADER_HEIGHT = 120; + scrollToElementTop(fileListSection, HEADER_HEIGHT); + } + } + // Avoiding read same image file + setTimeout(() => { + this._imageUploads.push(imageUpload); + window.viewModel.generatePreviewMockup(imageUpload); + }, i * 10); } } - async remove(filename, index) { - this._imageUploads = this._imageUploads.filter((upload, i) => { + async remove(filename, fileUlid) { + this._imageUploads = this._imageUploads.filter((upload) => { const isSameFilename = upload.file.name === filename; - const isSameIndex = i === index; - return !(isSameFilename && isSameIndex); + const isSameULID = fileUlid === upload.ulid; + return !(isSameFilename && isSameULID); + }); + + window.viewModel.selectedPreviewImageULID = + window.viewModel.defaultImageUploadULID; + } + + updateImageUploadStateByULID(ulid, state) { + this._imageUploads = this._imageUploads.map((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.state = state; + } + return imageUpload; + }); + } + + updateImageUploadPreviewUrlByULID(ulid, previewUrl) { + this._imageUploads = this._imageUploads.map((imageUpload) => { + if (imageUpload.ulid == ulid) { + imageUpload.previewUrl = previewUrl; + } + return imageUpload; }); } } @@ -106,10 +224,10 @@ class RootViewModel { fileList; isFileDragEnter = false; _isGeneratingMockup = false; - _socket = null; - _redirectTimer = null; worker = new Worker("/scripts/web_worker.js"); + previewWorker = new Worker("/scripts/preview_worker.js"); selectedColorId = null; + selectedPreviewImageULID = null; constructor(maxMockupWaitSec, fileListViewModel, selectedColorId) { mobx.makeObservable(this, { @@ -119,6 +237,7 @@ class RootViewModel { isGeneratingMockup: mobx.computed, generateMockup: mobx.action, cancelMockup: mobx.action, + selectedPreviewImageULID: mobx.observable, }); this.selectedColorId = selectedColorId; this.maxMockupWaitSec = maxMockupWaitSec; @@ -129,6 +248,10 @@ class RootViewModel { return this._isGeneratingMockup; } + async generatePreviewMockup(imageUpload) { + runPreviewWorker(this.previewWorker, imageUpload); + } + async generateMockup() { if (!this.fileList.isReadyForMockup) { console.warn("Cannot generate mockup at this moment"); @@ -138,37 +261,24 @@ class RootViewModel { runWorker(this.worker); } - _prepareMockup() { - this._scheduleRedirect(0); - } - cancelMockup() { if (!this.isGeneratingMockup) { return; } - this._cancelScheduleRedirect(); - this._socket = null; this._isGeneratingMockup = false; this.worker.terminate(); this.worker = new Worker("/scripts/web_worker.js"); } - _scheduleRedirect(sec) { - this._redirectTimer = setTimeout(() => { - window.location.replace(this.previewUrl); - }, sec * 1000); - } - - _cancelScheduleRedirect() { - if (this._redirectTimer != null) { - clearTimeout(this._redirectTimer); - this._redirectTimer = null; - } - } - get previewUrl() { return "/download/?deviceId=" + window.workerDeviceId; } + + get defaultImageUploadULID() { + return this.fileList.imageUploads.length > 0 + ? this.fileList.imageUploads[0].ulid + : null; + } } function preventDefault(node, events) { @@ -200,23 +310,33 @@ function dismissUploading() { uploading?.classList.add("d-none"); } -function findFileListItem(fileIndex) { +function findFileListItem(fileUlid) { const fileListNode = document.querySelector(".file-list"); const itemNodes = fileListNode.querySelectorAll(".file-list-item"); for (const itemNode of itemNodes) { - if (itemNode.dataset.fileIndex === String(fileIndex)) { + if (itemNode.dataset.fileUlid === String(fileUlid)) { return itemNode; } } return null; } -function appendInitialFileListItem(fileIndex, filename) { +function appendInitialFileListItem(fileUlid, filename) { const fileListNode = document.querySelector(".file-list"); const itemNode = document.createElement("li"); + + const fileInfoNode = document.createElement("div"); + const previewStateNode = document.createElement("div"); + + previewStateNode.classList.add("file-list-item__preview-state"); + itemNode.appendChild(previewStateNode); + + fileInfoNode.classList.add("file-list-item__file-info"); + itemNode.appendChild(fileInfoNode); + itemNode.classList.add("file-list-item"); - itemNode.dataset.fileIndex = fileIndex; + itemNode.dataset.fileUlid = fileUlid; const headerNode = document.createElement("div"); headerNode.classList.add("file-list-item__filename"); @@ -227,25 +347,19 @@ function appendInitialFileListItem(fileIndex, filename) { const crossNode = document.createElement("button"); crossNode.classList.add("file-list-item__cross"); - crossNode.onclick = async () => { - await window.viewModel.fileList.remove(filename, fileIndex); + crossNode.onclick = async (event) => { + // Prevent triggering of click event on parent node + event.stopPropagation(); + await window.viewModel.fileList.remove(filename, fileUlid); }; headerNode.appendChild(crossNode); - itemNode.appendChild(headerNode); - - itemNode.insertAdjacentHTML( + fileInfoNode.appendChild(headerNode); + fileInfoNode.insertAdjacentHTML( "beforeend", `

`, ); - itemNode.insertAdjacentHTML( - "beforeend", - `
-
-
`, - ); - return fileListNode.appendChild(itemNode); } @@ -256,23 +370,30 @@ function removeAllFileListItems() { function updateFileListItem(itemNode, imageUpload) { const hintNode = itemNode.querySelector(".file-list-item__hint"); - const progressFillNode = itemNode.querySelector( - ".file-list-item__progress-bar-fill", - ); + const previewNode = itemNode.querySelector(".file-list-item__preview-state"); + const fileInfoNode = itemNode.querySelector(".file-list-item__file-info"); + + function onSelectPreviewImage() { + const deviceSection = document.querySelector(".device"); + + // scroll to device section on mobile devices + if (window.innerWidth <= 992) { + const HEADER_HEIGHT = 80; + scrollToElementTop(deviceSection, HEADER_HEIGHT); + } + window.viewModel.selectedPreviewImageULID = imageUpload.ulid; + } // clear previous state itemNode.classList.remove( "file-list-item--done", "file-list-item--error", "file-list-item--warning", - // NOTE: do not remove progress state immediately so the progress bar can proceed to 100% before being removed - // "file-list-item--progress" + "file-list-item__previewable", ); - progressFillNode.classList.remove( - "file-list-item__progress-bar-fill--30", - "file-list-item__progress-bar-fill--60", - "file-list-item__progress-bar-fill--90", - "file-list-item__progress-bar-fill--100", + previewNode.classList.remove( + "file-list-item__preview_selected", + "file-list-item__preview_non_selected", ); /* Expected UI for each state @@ -282,7 +403,6 @@ function updateFileListItem(itemNode, imageUpload) { | warning icon | | | | y | | y | | done icon | | | | | y | | | hint text | error hint | error hint | | ratio hint | | ratio hint | -| progress bar | | | y | y | | | */ // const deviceData = passResolution(); @@ -303,33 +423,42 @@ function updateFileListItem(itemNode, imageUpload) { isSameAspectRatio(imageDim, recommendDim) || isSameAspectRatio(imageDimRotate, recommendDim); const shouldShowAspectRatioWarning = - imageUpload.readState !== ReadState.Reading && !isCorrectDim; + imageUpload.state !== ImageUploadState.Reading && !isCorrectDim; // Update status icon // error status has higher precedence over warning if (imageUpload.isErrorState) { - itemNode.classList.remove("file-list-item--progress"); + itemNode.classList.remove("file-list-item--loading"); itemNode.classList.add("file-list-item--error"); } else if (shouldShowAspectRatioWarning) { itemNode.classList.add("file-list-item--warning"); } + + if (imageUpload.isGeneratingPreviewState) { + itemNode.classList.remove("file-list-item--done"); + itemNode.classList.add("file-list-item--loading"); + } + if (imageUpload.isSuccessState) { - setTimeout(() => { - itemNode.classList.remove("file-list-item--progress"); - }, 10); - itemNode.classList.add("file-list-item--done"); - } else if (imageUpload.isProcessingState) { - itemNode.classList.add("file-list-item--progress"); + itemNode.classList.remove("file-list-item--loading"); + itemNode.classList.add( + "file-list-item--done", + "file-list-item__previewable", + ); } // update hint text if (imageUpload.isErrorState) { - switch (imageUpload.readState) { - case ReadState.ErrUnsupportedFileType: + switch (imageUpload.state) { + case ImageUploadState.ErrUnsupportedFileType: hintNode.innerText = "Supported file extensions: JPG, PNG or PSD."; break; - case ReadState.ErrExceedMaxFileSize: + case ImageUploadState.ErrExceedMaxFileSize: hintNode.innerText = `File size should be less than ${MAX_FILE_SIZE_READABLE}.`; break; - case ReadState.ErrRead: + case ImageUploadState.ErrPreview: + hintNode.innerText = + "Preview failed. Please upload the image again to retry."; + break; + case ImageUploadState.ErrRead: default: hintNode.innerText = "Something went wrong. Please try upload again or refresh the page."; @@ -338,24 +467,17 @@ function updateFileListItem(itemNode, imageUpload) { } else if (shouldShowAspectRatioWarning) { hintNode.innerText = `Uploaded file dimension (${imageDim.width} × ${imageDim.height} pixels) differs from ideal (${recommendDim.width} × ${recommendDim.height} pixels).`; } - // update progress bar - if (imageUpload.isProcessingState || imageUpload.isSuccessState) { - progressFillNode.classList.add("file-list-item__progress-bar-fill--30"); - switch (imageUpload.readState) { - case ReadState.ReadyForRead: - progressFillNode.classList.add("file-list-item__progress-bar-fill--60"); - break; - case ReadState.Reading: - progressFillNode.classList.add("file-list-item__progress-bar-fill--90"); - break; - case ReadState.ReadSuccess: - progressFillNode.classList.add( - "file-list-item__progress-bar-fill--100", - ); - break; - default: - break; + + // update preview button + if (imageUpload.isSuccessState) { + if (window.viewModel.selectedPreviewImageULID == imageUpload.ulid) { + previewNode.classList.add("file-list-item__preview_selected"); + } else { + previewNode.classList.add("file-list-item__preview_non_selected"); } + + previewNode.addEventListener("click", onSelectPreviewImage); + fileInfoNode.addEventListener("click", onSelectPreviewImage); } if (imageUpload.isSuccessState && !shouldShowAspectRatioWarning) { @@ -436,8 +558,6 @@ function registerUploadGuide() { function main() { const htmlNode = document.querySelector("html"); - const uploadSection = document.querySelector("#above-file-uploaded"); - const uploadBtn = document.querySelector(".upload-guide__browse-btn"); const uploadGuideHint = document.querySelector(".upload-guide__hint"); const fileInput = document.querySelector(".upload-guide__file-input"); const fileSectionHeading = document.querySelector(".file-uploaded__heading"); @@ -567,27 +687,22 @@ function main() { } }); - // observe fileListViewModel: imageUploads[].readState + // observe fileListViewModel: imageUploads[].state mobx.reaction( () => - viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, - ), + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), async () => { const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } - - // scroll to upload element on mobile devices - if (window.innerWidth <= 992) { - const HEADER_HEIGHT = 80; - scrollToElementTop(uploadSection, HEADER_HEIGHT); - } }, { equals: mobx.comparer.shallow, @@ -601,9 +716,12 @@ function main() { removeAllFileListItems(); // remove then re-render const imageUploads = viewModel.fileList.imageUploads; for (let i = 0; i < imageUploads.length; ++i) { - let itemNode = findFileListItem(i); + let itemNode = findFileListItem(imageUploads[i].ulid); if (itemNode == null) { - itemNode = appendInitialFileListItem(i, imageUploads[i].file.name); + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); } updateFileListItem(itemNode, imageUploads[i]); } @@ -613,15 +731,53 @@ function main() { }, ); + // observe viewModel: selectedPreviewImageULID + mobx.reaction( + () => viewModel.selectedPreviewImageULID, + () => { + // update preview area + const imageContainer = document.querySelector( + ".upload__device-image-rect", + ); + if (viewModel.selectedPreviewImageULID === null) { + imageContainer.style.backgroundImage = ""; + const imageUploadHints = document.querySelectorAll( + ".upload__device-hint", + ); + imageUploadHints.forEach((imageUploadHint) => { + imageUploadHint.style.display = "flex"; + }); + } + + removeAllFileListItems(); // remove then re-render + const imageUploads = viewModel.fileList.imageUploads; + for (let i = 0; i < imageUploads.length; ++i) { + let itemNode = findFileListItem(imageUploads[i].ulid); + if (itemNode == null) { + itemNode = appendInitialFileListItem( + imageUploads[i].ulid, + imageUploads[i].file.name, + ); + } + updateFileListItem(itemNode, imageUploads[i]); + + if ( + imageUploads[i].isSuccessState && + imageUploads[i].ulid == window.viewModel.selectedPreviewImageULID + ) { + imageContainer.style.backgroundImage = `url(${imageUploads[i].previewUrl})`; + } + } + }, + ); + if (isDebug) { - // observe fileListViewModel: imageUploads, imageUploads[].readState + // observe fileListViewModel: imageUploads, imageUploads[].state mobx.autorun(() => { console.log("file list:", mobx.toJS(viewModel.fileList.imageUploads)); console.log( "read states:", - viewModel.fileList.imageUploads.map( - (imageUpload) => imageUpload.readState, - ), + viewModel.fileList.imageUploads.map((imageUpload) => imageUpload.state), ); }); } diff --git a/public/scripts/web_worker.js b/public/scripts/web_worker.js index 94c6960..b45df70 100644 --- a/public/scripts/web_worker.js +++ b/public/scripts/web_worker.js @@ -36,7 +36,7 @@ async function runMockup(pyodide) { ); pyodide.runPython( ` - temp = image_process.save_image(output_img_path_list) + temp = image_process.save_images(output_img_path_list) `, { globals: pythonNamespace }, ); @@ -49,6 +49,7 @@ async function main() { pyodideObject = await pyodideObject; self["imageUploadList"] = event.data.imageUploadList; + self["imageUpload"] = undefined; self["locationKey"] = event.data.location; self["deviceId"] = event.data.deviceId; self["deviceInfo"] = event.data.deviceInfo; diff --git a/src/pages/about.astro b/src/pages/about.astro index 7d0f92f..d64cfb7 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -25,20 +25,28 @@ import BaseLayout from "../layouts/BaseLayout.astro";

About the creator

MockUPhone.com is developed by - Skymakers and is - open-sourced at https://github.com/oursky/mockuphone.comSkymakers and is open-sourced at https://github.com/oursky/mockuphone.com. We are a team of 70+ professionals dedicated to creating apps, designing, and providing custom software solutions for our clients, ranging from award-winning startups to NASDAQ-listed companies. At Skymakers, we offer innovative products like - Authgear, which provides - simple and scalable authentication solutions, and - FormX.ai, designed to automate data - extraction and eliminate manual work. Our expertise in software - development ensures that we deliver exceptional value to our clients. - Feel free to send us an enquiryAuthgear, which provides simple and scalable authentication solutions, and + FormX.ai, designed to automate data extraction and eliminate manual work. Our + expertise in software development ensures that we deliver exceptional + value to our clients. Feel free to send us an enquiry and discuss your big ideas with us!

diff --git a/src/pages/model/[model].astro b/src/pages/model/[model].astro index e52a028..59961ad 100644 --- a/src/pages/model/[model].astro +++ b/src/pages/model/[model].astro @@ -158,6 +158,7 @@ if (deviceDetail.imagePath != null && deviceDetail.imagePath.length >= 1) { +
diff --git a/src/scripts/brand.json b/src/scripts/brand.json index bb5005a..1d4800e 100644 --- a/src/scripts/brand.json +++ b/src/scripts/brand.json @@ -73,6 +73,7 @@ "galaxy-s4", "galaxy-note-5", "galaxy-y", + "galaxy-watch-4", "samsung-tv-d8000", "samsung-tv-es8000" ] diff --git a/src/scripts/device_info.json b/src/scripts/device_info.json index 985e1e4..7299906 100755 --- a/src/scripts/device_info.json +++ b/src/scripts/device_info.json @@ -2156,9 +2156,9 @@ }, { "credits": "

Meta - Design Resources

", - "color_str": "Gold", + "color_str": "Silver", "color": { - "id": "gold", + "id": "silver", "name": "Silver", "hexcode": "#F2F5F4" }, @@ -2543,8 +2543,7 @@ "name": "front" } ], - "available_perspectives": ["Front"], - "is_legacy": true + "available_perspectives": ["Front"] }, { "credits": "

Meta - Design Resources

", @@ -2572,8 +2571,7 @@ "name": "front" } ], - "available_perspectives": ["Front"], - "is_legacy": true + "available_perspectives": ["Front"] }, { "credits": "

Meta - Design Resources

", @@ -3171,8 +3169,7 @@ "name": "front" } ], - "available_perspectives": ["Front"], - "is_legacy": true + "available_perspectives": ["Front"] }, { "credits": "

Meta - Design Resources

", @@ -3200,8 +3197,7 @@ "name": "front" } ], - "available_perspectives": ["Front"], - "is_legacy": true + "available_perspectives": ["Front"] }, { "credits": "

Meta - Design Resources

", @@ -3229,8 +3225,7 @@ "name": "front" } ], - "available_perspectives": ["Front"], - "is_legacy": true + "available_perspectives": ["Front"] }, { "credits": "

Meta - Design Resources

", @@ -3268,8 +3263,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3307,8 +3301,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3346,8 +3339,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "background_class": "fish-bg2", @@ -3463,8 +3455,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3503,8 +3494,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3543,8 +3533,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3583,8 +3572,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3623,8 +3611,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Pixel Art Factory

", @@ -3748,8 +3735,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3788,8 +3774,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -3828,8 +3813,7 @@ "name": "landscape" } ], - "available_perspectives": ["Portrait", "Landscape"], - "is_legacy": true + "available_perspectives": ["Portrait", "Landscape"] }, { "credits": "

Meta - Design Resources

", @@ -6769,6 +6753,123 @@ ], "is_mockup_image_at_front": true, "available_perspectives": ["Portrait", "Landscape"] + }, + { + "credits": "

Skymaker's Designer

", + "color_str": "Black", + "color": { + "id": "black", + "name": "Black", + "hexcode": "#000000" + }, + "meta_title": "Samsung Galaxy Watch 4 Black Mock Up", + "meta_description": "1 click to generate your Samsung Galaxy Watch 4 Black mockup! You can wrap screenshots in Samsung Galaxy Watch 4 Black for prototypes.", + "display_resolution": [682, 682], + "device_type": "watchOS", + "device_id": "samsung-galaxy-watch-4-black", + "name": "Samsung Galaxy Watch 4", + "orientations": [ + { + "alt": "Samsung Galaxy Watch 4 Black Mock Up", + "coords": [ + [400, 405], + [1082, 405], + [1082, 1087], + [400, 1087] + ], + "name": "portrait" + }, + { + "alt": "Samsung Galaxy Watch 4 Black Mock Up", + "coords": [ + [1087, 400], + [1087, 1082], + [405, 1082], + [405, 400] + ], + "name": "landscape" + } + ], + "is_mockup_image_at_front": true, + "available_perspectives": ["Portrait", "Landscape"] + }, + { + "credits": "

Skymaker's Designer

", + "color_str": "Pink Gold", + "color": { + "id": "pink-gold", + "name": "Pink Gold", + "hexcode": "#FAE1C9" + }, + "meta_title": "Samsung Galaxy Watch 4 Pink Gold Mock Up", + "meta_description": "1 click to generate your Samsung Galaxy Watch 4 Pink Gold mockup! You can wrap screenshots in Samsung Galaxy Watch 4 Pink Gold for prototypes.", + "display_resolution": [682, 682], + "device_type": "watchOS", + "device_id": "samsung-galaxy-watch-4-pink-gold", + "name": "Samsung Galaxy Watch 4", + "orientations": [ + { + "alt": "Samsung Galaxy Watch 4 Pink Gold Mock Up", + "coords": [ + [400, 405], + [1082, 405], + [1082, 1087], + [400, 1087] + ], + "name": "portrait" + }, + { + "alt": "Samsung Galaxy Watch 4 Pink Gold Mock Up", + "coords": [ + [1087, 400], + [1087, 1082], + [405, 1082], + [405, 400] + ], + "name": "landscape" + } + ], + "is_mockup_image_at_front": true, + "available_perspectives": ["Portrait", "Landscape"] + }, + { + "credits": "

Skymaker's Designer

", + "color_str": "Silver", + "color": { + "id": "silver", + "name": "Silver", + "hexcode": "#D8D8D8" + }, + "meta_title": "Samsung Galaxy Watch 4 Silver Mock Up", + "meta_description": "1 click to generate your Samsung Galaxy Watch 4 Silver mockup! You can wrap screenshots in Samsung Galaxy Watch 4 Silver for prototypes.", + "display_resolution": [682, 682], + "device_type": "watchOS", + "device_id": "samsung-galaxy-watch-4-silver", + "name": "Samsung Galaxy Watch 4", + "orientations": [ + { + "alt": "Samsung Galaxy Watch 4 Silver Mock Up", + "coords": [ + [400, 405], + [1082, 405], + [1082, 1087], + [400, 1087] + ], + "name": "portrait" + }, + { + "alt": "Samsung Galaxy Watch 4 Silver Mock Up", + "coords": [ + [1087, 400], + [1087, 1082], + [405, 1082], + [405, 400] + ], + "name": "landscape" + } + ], + "is_mockup_image_at_front": true, + "available_perspectives": ["Portrait", "Landscape"] } ] } diff --git a/src/scripts/device_model.json b/src/scripts/device_model.json index 98d9f16..f32c755 100644 --- a/src/scripts/device_model.json +++ b/src/scripts/device_model.json @@ -557,5 +557,15 @@ "launchDateTimestamp": "2012-01-01T00:00:00Z", "type": "tv", "slugs": ["lg-tm2792"] + }, + "galaxy-watch-4": { + "name": "Samsung Galaxy Watch 4", + "launchDateTimestamp": "2021-08-27T00:00:00Z", + "type": "wearables", + "slugs": [ + "samsung-galaxy-watch-4-black", + "samsung-galaxy-watch-4-pink-gold", + "samsung-galaxy-watch-4-silver" + ] } } diff --git a/src/scripts/device_type.json b/src/scripts/device_type.json index 142eb82..40cc282 100644 --- a/src/scripts/device_type.json +++ b/src/scripts/device_type.json @@ -67,7 +67,8 @@ "apple-watch-ultra-2", "apple-watch-series-5-40mm", "apple-watch-series-8-41mm", - "apple-watch-series-8-45mm" + "apple-watch-series-8-45mm", + "galaxy-watch-4" ], "tv": ["samsung-tv-es8000", "samsung-tv-d8000", "lg-55lw5600", "lg-tm2792"] } diff --git a/src/scripts/parse/parseModel.ts b/src/scripts/parse/parseModel.ts index d1f7fd8..d99ed38 100644 --- a/src/scripts/parse/parseModel.ts +++ b/src/scripts/parse/parseModel.ts @@ -97,6 +97,8 @@ export const ModelEnum = z.enum([ "lg-55lw5600", "lg-tm2792", + + "galaxy-watch-4", ]); export type ModelEnum = z.infer; diff --git a/src/scripts/redirect.json b/src/scripts/redirect.json index c6cc669..8e43573 100644 --- a/src/scripts/redirect.json +++ b/src/scripts/redirect.json @@ -103,6 +103,9 @@ "/device/motorola-motoe-white": "/model/moto-e/color/white", "/device/motorola-motog": "/model/moto-g/color/dark", "/device/samsung-galaxy-s24-ultra": "/model/galaxy-s24-ultra/color/titanium-gray", + "/device/samsung-galaxy-watch-4-black": "/model/galaxy-watch-4/color/black", + "/device/samsung-galaxy-watch-4-pink-gold": "/model/galaxy-watch-4/color/pink-gold", + "/device/samsung-galaxy-watch-4-silver": "/model/galaxy-watch-4/color/silver", "/device/samsung-galaxys21plus-black": "/model/galaxy-s21-plus/color/black", "/device/samsung-galaxys21plus-silver": "/model/galaxy-s21-plus/color/silver", "/device/samsung-galaxys21plus-violet": "/model/galaxy-s21-plus/color/violet", diff --git a/src/styles/upload.css b/src/styles/upload.css index 79f0441..8eee99f 100644 --- a/src/styles/upload.css +++ b/src/styles/upload.css @@ -95,6 +95,7 @@ main { top: 0; left: 0; display: none; /* default hide */ + z-index: 10; } .drop-zone-overlay__show { @@ -147,6 +148,15 @@ main { width: 100%; height: 100%; object-fit: contain; + + /* Overlay the preview image with the color device image */ + position: relative; + z-index: 1; +} + +.upload__device-image-rect { + background-size: cover; + background-position: center; } .upload__device-image-rect-wrapper { @@ -160,10 +170,6 @@ main { justify-content: center; } -.upload__device-image-rect { - position: relative; -} - .upload__device-image-rect__screen-rect { position: absolute; } @@ -341,6 +347,58 @@ main { } .file-list-item { + display: flex; + + &.file-list-item__previewable { + gap: 4px; + + .file-list-item__file-info { + max-width: calc(100% - 28px); + } + + .file-list-item__preview-state { + cursor: pointer; + } + } +} + +@media (max-width: 992px) { + .file-list-item { + &.file-list-item__previewable { + .file-list-item__file-info { + cursor: pointer; + } + } + } +} + +.file-list-item_left { + width: 0; +} + +.file-list-item__preview_selected, +.file-list-item__preview_non_selected { + margin: 8px 0 0 0; + padding-inline: 6px; + width: 24px; + display: flex; + border-radius: 5px; + background-size: 12px 8px; + background-repeat: no-repeat; + background-position: center; +} + +.file-list-item__preview_selected { + background-image: url("/images/preview-white.svg"); + background-color: rgba(0, 67, 224, 1); +} + +.file-list-item__preview_non_selected { + background-image: url("/images/preview-gray.svg"); + background-color: white; +} + +.file-list-item__file-info { margin: 8px 0 0 0; border: 1px solid var(--gray-5); border-radius: 10px; @@ -349,6 +407,7 @@ main { display: flex; flex-direction: column; justify-content: center; + flex: 1; } .file-list-item__filename { @@ -409,44 +468,24 @@ main { background-image: url("/images/upload-error.svg"); } +.file-list-item--loading .file-list-item__filename::before { + content: ""; + display: block; + width: 20px; + height: 20px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + background-image: url("/images/spinner-blue-2.png"); + animation: spin 2s linear infinite; +} + .file-list-item__hint { margin: 5px 0 0 0; font-size: 12px; color: var(--gray-3); } -.file-list-item__progress-bar-border { - display: none; - margin: 5px 0 0 0; - width: 100%; - height: 6px; - background: rgba(0, 0, 0, 0.1); - border-radius: 30px; - overflow: hidden; -} -.file-list-item--progress .file-list-item__progress-bar-border { - display: block; -} - -.file-list-item__progress-bar-fill { - width: 0; - height: 100%; - background: var(--green-3); - transition: width 1s ease-in-out; -} -.file-list-item__progress-bar-fill--30 { - width: 30%; -} -.file-list-item__progress-bar-fill--60 { - width: 60%; -} -.file-list-item__progress-bar-fill--90 { - width: 90%; -} -.file-list-item__progress-bar-fill--100 { - width: 100%; -} - .color-section { margin: 20px 0 0 0; }