Compare commits
No commits in common. "main" and "refactor_pes" have entirely different histories.
main
...
refactor_p
|
|
@ -1,17 +0,0 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[{package.json,*.yml,*.js}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2022: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
processor: 'svelte3/svelte3',
|
||||
},
|
||||
],
|
||||
plugins: ['svelte'],
|
||||
settings: {
|
||||
// Let ESLint understand Svelte
|
||||
'svelte3/ignore-styles': () => true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
// Customize your rules here
|
||||
},
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.SSH_KEY }}" > ./deploy.key
|
||||
chmod 600 ./deploy.key
|
||||
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch .env
|
||||
echo EMAIL_ACCESS_KEY=${{ secrets.EMAIL_ACCESS_KEY }} >> .env
|
||||
echo EMAIL_BASE_URL=${{ secrets.EMAIL_BASE_URL }} >> .env
|
||||
|
||||
- name: Verify .env file creation
|
||||
run: cat .env
|
||||
|
||||
- name: Install PM2
|
||||
run: npm i -g pm2
|
||||
|
||||
- name: Deploy
|
||||
run: env $(cat .env | grep -v \"#\" | xargs) pm2 deploy ecosystem.config.cjs production
|
||||
27
.github/workflows/deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SSH_KEY: ${{secrets.SSH_KEY}}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: "npm"
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: mkdir ~/.ssh
|
||||
- run: echo "$SSH_KEY" >> ~/.ssh/id_rsa_embroideryviewer
|
||||
- run: chmod 400 ~/.ssh/id_rsa_embroideryviewer
|
||||
- run: echo -e "Host embroideryviewer\n\tUser embroideryviewer\n\tHostname 45.76.5.44\n\tIdentityFile ~/.ssh/id_rsa_embroideryviewer\n\tStrictHostKeyChecking No" >> ~/.ssh/config
|
||||
- run: rsync -avz --progress dist/ embroideryviewer:web/prod
|
||||
34
.gitignore
vendored
|
|
@ -1,11 +1,25 @@
|
|||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
deploy.key
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/.vscode
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# Ignore node_modules
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
.build/
|
||||
.svelte-kit/
|
||||
dist/
|
||||
|
||||
# Ignore lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# Ignore environment files
|
||||
.env
|
||||
.env.*.local
|
||||
|
||||
# VSCode settings
|
||||
.vscode/
|
||||
|
||||
# Ignore output from lint or test tools
|
||||
coverage/
|
||||
10
.prettierrc
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Leonardo Murça
|
||||
|
||||
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.
|
||||
73
README.md
|
|
@ -1,73 +1,10 @@
|
|||
# 🧵 Embroidery Viewer
|
||||
# Embroidery Viewer
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**The simplest way to preview embroidery files — instantly, in your browser.**
|
||||
|
||||
👉 **Try it now:** https://embroideryviewer.xyz
|
||||
A free online tool to view embroidery files.
|
||||
Available at https://embroideryviewer.xyz.
|
||||
|
||||

|
||||
|
||||
<a href="https://buymeacoffee.com/embroideryviewerxyz">
|
||||
<img src="docs/yellow-button.png" width="200" alt="Alt Text">
|
||||
</a>
|
||||
Current supported formats: **.pes, .dst, .pec, .jef and .exp**.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Why Embroidery Viewer?
|
||||
|
||||
Working with embroidery files shouldn’t require heavy, expensive software.
|
||||
|
||||
Embroidery Viewer was built to solve a simple problem:
|
||||
|
||||
> _“I just want to quickly see my design.”_
|
||||
|
||||
No installs. No friction. Just drag, drop, and view.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- ⚡ **Instant preview** — open files in seconds
|
||||
- 🔒 **Private by design** — everything runs in your browser
|
||||
- 🧵 **Multiple formats supported**
|
||||
- 🖥️ **Works on any device** (desktop, tablet, mobile)
|
||||
- 📂 **Batch-friendly** — view multiple files in sequence
|
||||
|
||||
---
|
||||
|
||||
## 📁 Supported Formats
|
||||
|
||||
- `.pes`
|
||||
- `.dst`
|
||||
- `.pec`
|
||||
- `.jef`
|
||||
- `.exp`
|
||||
|
||||
---
|
||||
|
||||
## 🧠 How it works
|
||||
|
||||
All processing happens **client-side** using modern web technologies.
|
||||
Your files are never uploaded to any server.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Inspiration
|
||||
|
||||
Inspired by:
|
||||
https://github.com/redteam316/html5-embroidery.git
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Support the project
|
||||
|
||||
If this tool helped you, consider supporting its development:
|
||||
|
||||
- Share it with others
|
||||
- Give feedback
|
||||
- Or contribute to the project
|
||||
|
||||
---
|
||||
Inspired by https://github.com/redteam316/html5-embroidery.git.
|
||||
|
|
|
|||
BIN
demo.gif
|
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 13 MiB |
|
Before Width: | Height: | Size: 24 KiB |
|
|
@ -1,38 +0,0 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'embroidery-viewer-prod',
|
||||
script: './build/index.js',
|
||||
time: true,
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
max_restarts: 50,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
env: {
|
||||
PORT: 7281,
|
||||
PUBLIC_APP_ENV: 'production',
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
deploy: {
|
||||
production: {
|
||||
user: 'deployer',
|
||||
host: '45.76.5.44',
|
||||
key: 'deploy.key',
|
||||
ref: 'origin/main',
|
||||
repo: 'git@git.leomurca.xyz:leomurca/embroidery-viewer.git',
|
||||
path: '/home/deployer/embroidery-viewer',
|
||||
'pre-deploy':
|
||||
'rm -rf node_modules build .svelte-kit && npm ci && PUBLIC_APP_ENV=production npm run build',
|
||||
'post-deploy':
|
||||
'pm2 startOrReload ecosystem.config.cjs --only embroidery-viewer-prod --env production && pm2 save',
|
||||
env: {
|
||||
PORT: 7281,
|
||||
PUBLIC_APP_ENV: 'production',
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default [
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.js'],
|
||||
languageOptions: { parserOptions: { svelteConfig } }
|
||||
}
|
||||
];
|
||||
42
index.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="author" content="Leonardo Murça" />
|
||||
<meta name="description" content="Free online embroidery viewer.">
|
||||
<meta name="keywords"
|
||||
content="Free Emrbroidery Viewer, embroidery design, sewing machine, preview .pes files, preview embroider designs, brother machine.">
|
||||
|
||||
<script async defer data-website-id="e9089f5e-32ea-45f1-a000-b16af1dba58a"
|
||||
src="https://umami.leomurca.xyz/umami.js"></script>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>Embroidery Viewer</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,19 +1,33 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "Node",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
/**
|
||||
* svelte-preprocess cannot figure out whether you have
|
||||
* a value or a type, so tell TypeScript to enforce using
|
||||
* `import type` instead of `import` for Types.
|
||||
*/
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* To have warnings / errors of the Svelte compiler at the
|
||||
* correct position, enable source maps by default.
|
||||
*/
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable this if you'd like to use dynamic types.
|
||||
*/
|
||||
"checkJs": true
|
||||
},
|
||||
/**
|
||||
* Use global.d.ts instead of compilerOptions.types
|
||||
* to avoid limiting type declarations.
|
||||
*/
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
|
|
|
|||
BIN
logo.webp
|
Before Width: | Height: | Size: 3.2 KiB |
3655
package-lock.json
generated
33
package.json
|
|
@ -1,38 +1,17 @@
|
|||
{
|
||||
"name": "embroidery-viewer",
|
||||
"private": true,
|
||||
"version": "3.0.3",
|
||||
"version": "1.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"postbuild": "npx svelte-sitemap --domain https://embroideryviewer.xyz -o dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/kit": "^2.57.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.17.0",
|
||||
"globals": "^17.5.0",
|
||||
"prettier": "^3.8.3",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.55.4",
|
||||
"svelte-check": "^4.4.6",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"sveltekit-i18n": "^2.4.2"
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.1",
|
||||
"svelte": "^3.59.1",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
public/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/apple-icon-114x114.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/apple-icon-120x120.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/apple-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/apple-icon-152x152.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/apple-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-icon-57x57.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/apple-icon-60x60.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/apple-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/apple-icon-76x76.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/apple-icon-precomposed.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
2
public/browserconfig.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
||||
BIN
public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
41
public/manifest.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "\/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "\/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
public/ms-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/ms-icon-150x150.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/ms-icon-310x310.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/ms-icon-70x70.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
22
src/App.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import Header from "./lib/Header.svelte";
|
||||
import FileViewer from "./lib/FileViewer.svelte";
|
||||
import Footer from "./lib/Footer.svelte";
|
||||
</script>
|
||||
|
||||
<Header />
|
||||
<main>
|
||||
<FileViewer />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 15px;
|
||||
}
|
||||
</style>
|
||||
50
src/app.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #F2F6F5;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
margin-top: 20px;
|
||||
background-color: #05345f;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
cursor: pointer;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
13
src/app.d.ts
vendored
|
|
@ -1,13 +0,0 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
47
src/app.html
|
|
@ -1,47 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script
|
||||
data-name="BMC-Widget"
|
||||
data-cfasync="false"
|
||||
src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"
|
||||
data-id="embroideryviewerxyz"
|
||||
data-description="Support me on Buy me a coffee!"
|
||||
data-message="Enjoying Embroidery Viewer?
|
||||
Buy me a coffee and help keep it running ☕"
|
||||
data-color="#FFDD03"
|
||||
data-position="Right"
|
||||
data-x_margin="18"
|
||||
data-y_margin="18"
|
||||
></script>
|
||||
<!-- Basic -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="author" content="Embroidery Viewer" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<link rel="canonical" href="https://embroideryviewer.xyz/" />
|
||||
|
||||
<!-- Mobile / PWA friendliness -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/merienda.regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
src/assets/embroidery-viewer-logo-mobile.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/embroidery-viewer-logo.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/thumbnail.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
74
src/file-renderer/index.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { jDataView } from "./jdataview";
|
||||
import { supportedFormats } from "../format-readers";
|
||||
import { Pattern } from "./pattern";
|
||||
|
||||
function renderFile(filename, evt, canvas, colorView, stitchesView, sizeView) {
|
||||
const fileExtension = filename.toLowerCase().split(".").pop();
|
||||
const view = jDataView(evt.target.result, 0, evt.size);
|
||||
const pattern = new Pattern();
|
||||
|
||||
supportedFormats[fileExtension].read(view, pattern);
|
||||
|
||||
pattern.moveToPositive();
|
||||
pattern.drawShapeTo(canvas);
|
||||
pattern.drawColorsTo(colorView);
|
||||
pattern.drawStitchesCountTo(stitchesView);
|
||||
pattern.drawSizeValuesTo(stitchesView);
|
||||
}
|
||||
|
||||
function renderAbortMessage(errorMessageRef) {
|
||||
errorMessageRef.innerHTML = "Render aborted!";
|
||||
}
|
||||
|
||||
function renderErrorMessage(errorName, errorMessageRef) {
|
||||
let message;
|
||||
switch (errorName) {
|
||||
case "NotFoundError":
|
||||
message =
|
||||
"The file could not be found at the time the read was processed.";
|
||||
break;
|
||||
case "SecurityError":
|
||||
message = "<p>A file security error occured. This can be due to:</p>";
|
||||
message +=
|
||||
"<ul><li>Accessing certain files deemed unsafe for Web applications.</li>";
|
||||
message += "<li>Performing too many read calls on file resources.</li>";
|
||||
message +=
|
||||
"<li>The file has changed on disk since the user selected it.</li></ul>";
|
||||
break;
|
||||
case "NotReadableError":
|
||||
message =
|
||||
"The file cannot be read. This can occur if the file is open in another application.";
|
||||
break;
|
||||
case "EncodingError":
|
||||
message = "The length of the data URL for the file is too long.";
|
||||
break;
|
||||
default:
|
||||
message = "Something wrong happened!";
|
||||
break;
|
||||
}
|
||||
|
||||
errorMessageRef.innerHTML = message;
|
||||
}
|
||||
|
||||
export default function renderFileToCanvas(
|
||||
fileObject,
|
||||
canvas,
|
||||
errorMessageRef,
|
||||
colorView,
|
||||
stitchesView,
|
||||
sizeView
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = (evt) =>
|
||||
renderFile(fileObject.name, evt, canvas, colorView, stitchesView, sizeView);
|
||||
reader.abort = (/** @type {any} */ _) => renderAbortMessage(errorMessageRef);
|
||||
reader.onerror = (evt) =>
|
||||
renderErrorMessage(evt.target.error.name, errorMessageRef);
|
||||
|
||||
if (fileObject) {
|
||||
reader.readAsArrayBuffer(fileObject);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-undef */
|
||||
// @ts-nocheck
|
||||
import { browser } from '$app/environment';
|
||||
//
|
||||
// jDataView by Vjeux <vjeuxx@gmail.com> - Jan 2010
|
||||
// Continued by RReverser <me@rreverser.com> - Feb 2013
|
||||
//
|
||||
|
|
@ -11,22 +9,22 @@ import { browser } from '$app/environment';
|
|||
|
||||
var compatibility = {
|
||||
// NodeJS Buffer in v0.5.5 and newer
|
||||
NodeBuffer: 'Buffer' in globalThis && 'readInt16LE' in Buffer.prototype,
|
||||
NodeBuffer: "Buffer" in globalThis && "readInt16LE" in Buffer.prototype,
|
||||
DataView:
|
||||
'DataView' in globalThis &&
|
||||
('getFloat64' in DataView.prototype || // Chrome
|
||||
'getFloat64' in new DataView(new ArrayBuffer(1))), // Node
|
||||
ArrayBuffer: 'ArrayBuffer' in globalThis,
|
||||
"DataView" in globalThis &&
|
||||
("getFloat64" in DataView.prototype || // Chrome
|
||||
"getFloat64" in new DataView(new ArrayBuffer(1))), // Node
|
||||
ArrayBuffer: "ArrayBuffer" in globalThis,
|
||||
PixelData:
|
||||
'CanvasPixelArray' in globalThis &&
|
||||
'ImageData' in globalThis &&
|
||||
'document' in globalThis,
|
||||
"CanvasPixelArray" in globalThis &&
|
||||
"ImageData" in globalThis &&
|
||||
"document" in globalThis,
|
||||
};
|
||||
|
||||
var createPixelData = function (byteLength, buffer) {
|
||||
var data = createPixelData.context2d.createImageData(
|
||||
(byteLength + 3) / 4,
|
||||
1,
|
||||
1
|
||||
).data;
|
||||
data.byteLength = byteLength;
|
||||
if (buffer !== undefined) {
|
||||
|
|
@ -36,8 +34,7 @@ var createPixelData = function (byteLength, buffer) {
|
|||
}
|
||||
return data;
|
||||
};
|
||||
createPixelData.context2d =
|
||||
browser ?? document.createElement('canvas').getContext('2d');
|
||||
createPixelData.context2d = document.createElement("canvas").getContext("2d");
|
||||
|
||||
var dataTypes = {
|
||||
Int8: 1,
|
||||
|
|
@ -51,14 +48,14 @@ var dataTypes = {
|
|||
};
|
||||
|
||||
var nodeNaming = {
|
||||
Int8: 'Int8',
|
||||
Int16: 'Int16',
|
||||
Int32: 'Int32',
|
||||
Uint8: 'UInt8',
|
||||
Uint16: 'UInt16',
|
||||
Uint32: 'UInt32',
|
||||
Float32: 'Float',
|
||||
Float64: 'Double',
|
||||
Int8: "Int8",
|
||||
Int16: "Int16",
|
||||
Int32: "Int32",
|
||||
Uint8: "UInt8",
|
||||
Uint16: "UInt16",
|
||||
Uint32: "UInt32",
|
||||
Float32: "Float",
|
||||
Float64: "Double",
|
||||
};
|
||||
|
||||
function arrayFrom(arrayLike, forceCopy) {
|
||||
|
|
@ -101,13 +98,13 @@ export function jDataView(buffer, byteOffset, byteLength, littleEndian) {
|
|||
!this._isPixelData &&
|
||||
!(buffer instanceof Array)
|
||||
) {
|
||||
throw new TypeError('jDataView buffer has an incompatible type');
|
||||
throw new TypeError("jDataView buffer has an incompatible type");
|
||||
}
|
||||
|
||||
// Default Values
|
||||
this._littleEndian = !!littleEndian;
|
||||
|
||||
var bufferLength = 'byteLength' in buffer ? buffer.byteLength : buffer.length;
|
||||
var bufferLength = "byteLength" in buffer ? buffer.byteLength : buffer.length;
|
||||
this.byteOffset = byteOffset = defined(byteOffset, 0);
|
||||
this.byteLength = byteLength = defined(byteLength, bufferLength - byteOffset);
|
||||
|
||||
|
|
@ -122,15 +119,15 @@ export function jDataView(buffer, byteOffset, byteLength, littleEndian) {
|
|||
this._engineAction = this._isDataView
|
||||
? this._dataViewAction
|
||||
: this._isNodeBuffer
|
||||
? this._nodeBufferAction
|
||||
: this._isArrayBuffer
|
||||
? this._arrayBufferAction
|
||||
: this._arrayAction;
|
||||
? this._nodeBufferAction
|
||||
: this._isArrayBuffer
|
||||
? this._arrayBufferAction
|
||||
: this._arrayAction;
|
||||
}
|
||||
|
||||
function getCharCodes(string) {
|
||||
if (compatibility.NodeBuffer) {
|
||||
return new Buffer(string, 'binary');
|
||||
return new Buffer(string, "binary");
|
||||
}
|
||||
|
||||
var Type = compatibility.ArrayBuffer ? Uint8Array : Array,
|
||||
|
|
@ -145,7 +142,7 @@ function getCharCodes(string) {
|
|||
// mostly internal function for wrapping any supported input (String or Array-like) to best suitable buffer format
|
||||
jDataView.wrapBuffer = function (buffer) {
|
||||
switch (typeof buffer) {
|
||||
case 'number':
|
||||
case "number":
|
||||
if (compatibility.NodeBuffer) {
|
||||
buffer = new Buffer(buffer);
|
||||
buffer.fill(0);
|
||||
|
|
@ -161,12 +158,12 @@ jDataView.wrapBuffer = function (buffer) {
|
|||
}
|
||||
return buffer;
|
||||
|
||||
case 'string':
|
||||
case "string":
|
||||
buffer = getCharCodes(buffer);
|
||||
/* falls through */
|
||||
default:
|
||||
if (
|
||||
'length' in buffer &&
|
||||
"length" in buffer &&
|
||||
!(
|
||||
(compatibility.NodeBuffer && buffer instanceof Buffer) ||
|
||||
(compatibility.ArrayBuffer && buffer instanceof ArrayBuffer) ||
|
||||
|
|
@ -233,7 +230,7 @@ function Int64(lo, hi) {
|
|||
jDataView.Int64 = Int64;
|
||||
|
||||
Int64.prototype =
|
||||
'create' in Object ? Object.create(Uint64.prototype) : new Uint64();
|
||||
"create" in Object ? Object.create(Uint64.prototype) : new Uint64();
|
||||
|
||||
Int64.prototype.valueOf = function () {
|
||||
if (this.hi < pow2(31)) {
|
||||
|
|
@ -264,20 +261,20 @@ jDataView.prototype = {
|
|||
|
||||
_checkBounds: function (byteOffset, byteLength, maxLength) {
|
||||
// Do additional checks to simulate DataView
|
||||
if (typeof byteOffset !== 'number') {
|
||||
throw new TypeError('Offset is not a number.');
|
||||
if (typeof byteOffset !== "number") {
|
||||
throw new TypeError("Offset is not a number.");
|
||||
}
|
||||
if (typeof byteLength !== 'number') {
|
||||
throw new TypeError('Size is not a number.');
|
||||
if (typeof byteLength !== "number") {
|
||||
throw new TypeError("Size is not a number.");
|
||||
}
|
||||
if (byteLength < 0) {
|
||||
throw new RangeError('Length is negative.');
|
||||
throw new RangeError("Length is negative.");
|
||||
}
|
||||
if (
|
||||
byteOffset < 0 ||
|
||||
byteOffset + byteLength > defined(maxLength, this.byteLength)
|
||||
) {
|
||||
throw new RangeError('Offsets are out of bounds.');
|
||||
throw new RangeError("Offsets are out of bounds.");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -287,7 +284,7 @@ jDataView.prototype = {
|
|||
isReadAction,
|
||||
defined(byteOffset, this._offset),
|
||||
defined(littleEndian, this._littleEndian),
|
||||
value,
|
||||
value
|
||||
);
|
||||
},
|
||||
|
||||
|
|
@ -296,13 +293,13 @@ jDataView.prototype = {
|
|||
isReadAction,
|
||||
byteOffset,
|
||||
littleEndian,
|
||||
value,
|
||||
value
|
||||
) {
|
||||
// Move the internal offset forward
|
||||
this._offset = byteOffset + dataTypes[type];
|
||||
return isReadAction
|
||||
? this._view['get' + type](byteOffset, littleEndian)
|
||||
: this._view['set' + type](byteOffset, value, littleEndian);
|
||||
? this._view["get" + type](byteOffset, littleEndian)
|
||||
: this._view["set" + type](byteOffset, value, littleEndian);
|
||||
},
|
||||
|
||||
_nodeBufferAction: function (
|
||||
|
|
@ -310,17 +307,17 @@ jDataView.prototype = {
|
|||
isReadAction,
|
||||
byteOffset,
|
||||
littleEndian,
|
||||
value,
|
||||
value
|
||||
) {
|
||||
// Move the internal offset forward
|
||||
this._offset = byteOffset + dataTypes[type];
|
||||
var nodeName =
|
||||
nodeNaming[type] +
|
||||
(type === 'Int8' || type === 'Uint8' ? '' : littleEndian ? 'LE' : 'BE');
|
||||
(type === "Int8" || type === "Uint8" ? "" : littleEndian ? "LE" : "BE");
|
||||
byteOffset += this.byteOffset;
|
||||
return isReadAction
|
||||
? this.buffer['read' + nodeName](byteOffset)
|
||||
: this.buffer['write' + nodeName](value, byteOffset);
|
||||
? this.buffer["read" + nodeName](byteOffset)
|
||||
: this.buffer["write" + nodeName](value, byteOffset);
|
||||
},
|
||||
|
||||
_arrayBufferAction: function (
|
||||
|
|
@ -328,10 +325,10 @@ jDataView.prototype = {
|
|||
isReadAction,
|
||||
byteOffset,
|
||||
littleEndian,
|
||||
value,
|
||||
value
|
||||
) {
|
||||
var size = dataTypes[type],
|
||||
TypedArray = globalThis[type + 'Array'],
|
||||
TypedArray = globalThis[type + "Array"],
|
||||
typedArray;
|
||||
|
||||
littleEndian = defined(littleEndian, this._littleEndian);
|
||||
|
|
@ -348,7 +345,7 @@ jDataView.prototype = {
|
|||
var bytes = new Uint8Array(
|
||||
isReadAction
|
||||
? this.getBytes(size, byteOffset, littleEndian, true)
|
||||
: size,
|
||||
: size
|
||||
);
|
||||
typedArray = new TypedArray(bytes.buffer, 0, 1);
|
||||
|
||||
|
|
@ -363,8 +360,8 @@ jDataView.prototype = {
|
|||
|
||||
_arrayAction: function (type, isReadAction, byteOffset, littleEndian, value) {
|
||||
return isReadAction
|
||||
? this['_get' + type](byteOffset, littleEndian)
|
||||
: this['_set' + type](byteOffset, value, littleEndian);
|
||||
? this["_get" + type](byteOffset, littleEndian)
|
||||
: this["_set" + type](byteOffset, value, littleEndian);
|
||||
},
|
||||
|
||||
// Helpers
|
||||
|
|
@ -385,7 +382,7 @@ jDataView.prototype = {
|
|||
: (this.buffer.slice || Array.prototype.slice).call(
|
||||
this.buffer,
|
||||
byteOffset,
|
||||
byteOffset + length,
|
||||
byteOffset + length
|
||||
);
|
||||
|
||||
return littleEndian || length <= 1 ? result : arrayFrom(result).reverse();
|
||||
|
|
@ -396,7 +393,7 @@ jDataView.prototype = {
|
|||
var result = this._getBytes(
|
||||
length,
|
||||
byteOffset,
|
||||
defined(littleEndian, true),
|
||||
defined(littleEndian, true)
|
||||
);
|
||||
return toArray ? arrayFrom(result) : result;
|
||||
},
|
||||
|
|
@ -448,18 +445,18 @@ jDataView.prototype = {
|
|||
|
||||
this._offset = byteOffset + byteLength;
|
||||
return this.buffer.toString(
|
||||
encoding || 'binary',
|
||||
encoding || "binary",
|
||||
this.byteOffset + byteOffset,
|
||||
this.byteOffset + this._offset,
|
||||
this.byteOffset + this._offset
|
||||
);
|
||||
}
|
||||
var bytes = this._getBytes(byteLength, byteOffset, true),
|
||||
string = '';
|
||||
string = "";
|
||||
byteLength = bytes.length;
|
||||
for (var i = 0; i < byteLength; i++) {
|
||||
string += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
if (encoding === 'utf8') {
|
||||
if (encoding === "utf8") {
|
||||
string = decodeURIComponent(escape(string));
|
||||
}
|
||||
return string;
|
||||
|
|
@ -474,11 +471,11 @@ jDataView.prototype = {
|
|||
this.buffer.write(
|
||||
subString,
|
||||
this.byteOffset + byteOffset,
|
||||
encoding || 'binary',
|
||||
encoding || "binary"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (encoding === 'utf8') {
|
||||
if (encoding === "utf8") {
|
||||
subString = unescape(encodeURIComponent(subString));
|
||||
}
|
||||
this._setBytes(byteOffset, getCharCodes(subString), true);
|
||||
|
|
@ -519,13 +516,13 @@ jDataView.prototype = {
|
|||
this.getBytes(end - start, start, true, true),
|
||||
undefined,
|
||||
undefined,
|
||||
this._littleEndian,
|
||||
this._littleEndian
|
||||
)
|
||||
: new jDataView(
|
||||
this.buffer,
|
||||
this.byteOffset + start,
|
||||
end - start,
|
||||
this._littleEndian,
|
||||
this._littleEndian
|
||||
);
|
||||
},
|
||||
|
||||
|
|
@ -682,7 +679,7 @@ jDataView.prototype = {
|
|||
value,
|
||||
mantSize,
|
||||
expSize,
|
||||
littleEndian,
|
||||
littleEndian
|
||||
) {
|
||||
var signBit = value < 0 ? 1 : 0,
|
||||
exponent,
|
||||
|
|
@ -754,7 +751,7 @@ jDataView.prototype = {
|
|||
this.setUint32(
|
||||
byteOffset + parts[partName],
|
||||
value[partName],
|
||||
littleEndian,
|
||||
littleEndian
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -773,7 +770,7 @@ jDataView.prototype = {
|
|||
this._setBytes(
|
||||
byteOffset,
|
||||
[value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, value >>> 24],
|
||||
littleEndian,
|
||||
littleEndian
|
||||
);
|
||||
},
|
||||
|
||||
|
|
@ -781,7 +778,7 @@ jDataView.prototype = {
|
|||
this._setBytes(
|
||||
byteOffset,
|
||||
[value & 0xff, (value >>> 8) & 0xff],
|
||||
littleEndian,
|
||||
littleEndian
|
||||
);
|
||||
},
|
||||
|
||||
|
|
@ -811,10 +808,10 @@ var proto = jDataView.prototype;
|
|||
|
||||
for (var type in dataTypes) {
|
||||
(function (type) {
|
||||
proto['get' + type] = function (byteOffset, littleEndian) {
|
||||
proto["get" + type] = function (byteOffset, littleEndian) {
|
||||
return this._action(type, true, byteOffset, littleEndian);
|
||||
};
|
||||
proto['set' + type] = function (byteOffset, value, littleEndian) {
|
||||
proto["set" + type] = function (byteOffset, value, littleEndian) {
|
||||
this._action(type, false, byteOffset, littleEndian, value);
|
||||
};
|
||||
})(type);
|
||||
|
|
@ -826,19 +823,19 @@ proto._setInt8 = proto._setUint8;
|
|||
proto.setSigned = proto.setUnsigned;
|
||||
|
||||
for (var method in proto) {
|
||||
if (method.slice(0, 3) === 'set') {
|
||||
if (method.slice(0, 3) === "set") {
|
||||
(function (type) {
|
||||
proto['write' + type] = function () {
|
||||
proto["write" + type] = function () {
|
||||
Array.prototype.unshift.call(arguments, undefined);
|
||||
this['set' + type].apply(this, arguments);
|
||||
this["set" + type].apply(this, arguments);
|
||||
};
|
||||
})(method.slice(3));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports === 'object') {
|
||||
if (typeof module !== "undefined" && typeof module.exports === "object") {
|
||||
module.exports = jDataView;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
} else if (typeof define === "function" && define.amd) {
|
||||
define([], function () {
|
||||
return jDataView;
|
||||
});
|
||||
225
src/file-renderer/pattern.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import { rgbToHex } from "../utils/rgbToHex";
|
||||
import { shadeColor } from "../utils/shadeColor";
|
||||
|
||||
function Stitch(x, y, flags, color) {
|
||||
this.flags = flags;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
function Color(r, g, b, description) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
const stitchTypes = {
|
||||
normal: 0,
|
||||
jump: 1,
|
||||
trim: 2,
|
||||
stop: 4,
|
||||
end: 8,
|
||||
};
|
||||
|
||||
function Pattern() {
|
||||
this.colors = [];
|
||||
this.stitches = [];
|
||||
this.hoop = {};
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.top = 0;
|
||||
this.bottom = 0;
|
||||
this.left = 0;
|
||||
this.right = 0;
|
||||
this.currentColorIndex = 0;
|
||||
}
|
||||
|
||||
Pattern.prototype.addColorRgb = function (r, g, b, description) {
|
||||
this.colors[this.colors.length] = new Color(r, g, b, description);
|
||||
};
|
||||
|
||||
Pattern.prototype.addColor = function (color) {
|
||||
this.colors[this.colors.length] = color;
|
||||
};
|
||||
|
||||
Pattern.prototype.addStitchAbs = function (x, y, flags, isAutoColorIndex) {
|
||||
if ((flags & stitchTypes.end) === stitchTypes.end) {
|
||||
this.calculateBoundingBox();
|
||||
this.fixColorCount();
|
||||
}
|
||||
if (
|
||||
(flags & stitchTypes.stop) === stitchTypes.stop &&
|
||||
this.stitches.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if ((flags & stitchTypes.stop) === stitchTypes.stop && isAutoColorIndex) {
|
||||
this.currentColorIndex += 1;
|
||||
}
|
||||
this.stitches[this.stitches.length] = new Stitch(
|
||||
x,
|
||||
y,
|
||||
flags,
|
||||
this.currentColorIndex
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.addStitchRel = function (dx, dy, flags, isAutoColorIndex) {
|
||||
if (this.stitches.length !== 0) {
|
||||
let nx = this.lastX + dx,
|
||||
ny = this.lastY + dy;
|
||||
this.lastX = nx;
|
||||
this.lastY = ny;
|
||||
this.addStitchAbs(nx, ny, flags, isAutoColorIndex);
|
||||
} else {
|
||||
this.addStitchAbs(dx, dy, flags, isAutoColorIndex);
|
||||
}
|
||||
};
|
||||
|
||||
Pattern.prototype.calculateBoundingBox = function () {
|
||||
let i = 0,
|
||||
stitchCount = this.stitches.length,
|
||||
pt;
|
||||
if (stitchCount === 0) {
|
||||
this.bottom = 1;
|
||||
this.right = 1;
|
||||
return;
|
||||
}
|
||||
this.left = 99999;
|
||||
this.top = 99999;
|
||||
this.right = -99999;
|
||||
this.bottom = -99999;
|
||||
|
||||
for (i = 0; i < stitchCount; i += 1) {
|
||||
pt = this.stitches[i];
|
||||
if (!(pt.flags & stitchTypes.trim)) {
|
||||
this.left = this.left < pt.x ? this.left : pt.x;
|
||||
this.top = this.top < pt.y ? this.top : pt.y;
|
||||
this.right = this.right > pt.x ? this.right : pt.x;
|
||||
this.bottom = this.bottom > pt.y ? this.bottom : pt.y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Pattern.prototype.moveToPositive = function () {
|
||||
let i = 0,
|
||||
stitchCount = this.stitches.length;
|
||||
for (i = 0; i < stitchCount; i += 1) {
|
||||
this.stitches[i].x -= this.left;
|
||||
this.stitches[i].y -= this.top;
|
||||
}
|
||||
this.right -= this.left;
|
||||
this.left = 0;
|
||||
this.bottom -= this.top;
|
||||
this.top = 0;
|
||||
};
|
||||
|
||||
Pattern.prototype.invertPatternVertical = function () {
|
||||
let i = 0,
|
||||
temp = -this.top,
|
||||
stitchCount = this.stitches.length;
|
||||
for (i = 0; i < stitchCount; i += 1) {
|
||||
this.stitches[i].y = -this.stitches[i].y;
|
||||
}
|
||||
this.top = -this.bottom;
|
||||
this.bottom = temp;
|
||||
};
|
||||
|
||||
Pattern.prototype.addColorRandom = function () {
|
||||
this.colors[this.colors.length] = new Color(
|
||||
Math.round(Math.random() * 256),
|
||||
Math.round(Math.random() * 256),
|
||||
Math.round(Math.random() * 256),
|
||||
"random"
|
||||
);
|
||||
};
|
||||
|
||||
Pattern.prototype.fixColorCount = function () {
|
||||
let maxColorIndex = 0,
|
||||
stitchCount = this.stitches.length,
|
||||
i;
|
||||
for (i = 0; i < stitchCount; i += 1) {
|
||||
maxColorIndex = Math.max(maxColorIndex, this.stitches[i].color);
|
||||
}
|
||||
while (this.colors.length <= maxColorIndex) {
|
||||
this.addColorRandom();
|
||||
}
|
||||
this.colors.splice(maxColorIndex + 1, this.colors.length - maxColorIndex - 1);
|
||||
};
|
||||
|
||||
Pattern.prototype.drawShapeTo = function (canvas) {
|
||||
canvas.width = this.right;
|
||||
canvas.height = this.bottom;
|
||||
|
||||
let gradient, tx, ty;
|
||||
let lastStitch = this.stitches[0];
|
||||
let gWidth = 100;
|
||||
if (canvas.getContext) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineJoin = "round";
|
||||
|
||||
let color = this.colors[this.stitches[0].color];
|
||||
for (let i = 0; i < this.stitches.length; i++) {
|
||||
const currentStitch = this.stitches[i];
|
||||
if (i > 0) lastStitch = this.stitches[i - 1];
|
||||
tx = currentStitch.x - lastStitch.x;
|
||||
ty = currentStitch.y - lastStitch.y;
|
||||
|
||||
gWidth = Math.sqrt(tx * tx + ty * ty);
|
||||
gradient = ctx.createRadialGradient(
|
||||
currentStitch.x - tx,
|
||||
currentStitch.y - ty,
|
||||
0,
|
||||
currentStitch.x - tx,
|
||||
currentStitch.y - ty,
|
||||
gWidth * 1.4
|
||||
);
|
||||
|
||||
gradient.addColorStop("0", shadeColor(rgbToHex(color), -60));
|
||||
gradient.addColorStop("0.05", rgbToHex(color));
|
||||
gradient.addColorStop("0.5", shadeColor(rgbToHex(color), 60));
|
||||
gradient.addColorStop("0.9", rgbToHex(color));
|
||||
gradient.addColorStop("1.0", shadeColor(rgbToHex(color), -60));
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
if (
|
||||
currentStitch.flags === stitchTypes.jump ||
|
||||
currentStitch.flags === stitchTypes.trim ||
|
||||
currentStitch.flags === stitchTypes.stop
|
||||
) {
|
||||
color = this.colors[currentStitch.color];
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle =
|
||||
"rgba(" + color.r + "," + color.g + "," + color.b + ",0)";
|
||||
ctx.moveTo(currentStitch.x, currentStitch.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastStitch.x, lastStitch.y);
|
||||
ctx.lineTo(currentStitch.x, currentStitch.y);
|
||||
ctx.stroke();
|
||||
lastStitch = currentStitch;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Pattern.prototype.drawColorsTo = function (colorContainer) {
|
||||
this.colors.forEach((color) => {
|
||||
colorContainer.innerHTML += `<div style='background-color: rgb(${color.r}, ${color.g}, ${color.b}); height: 25px; width: 25px; border: 1px solid #000000;'></div>`;
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.drawStitchesCountTo = function (stitchesContainer) {
|
||||
stitchesContainer.innerHTML += `<div><strong>Stitches:</strong> ${this.stitches.length} </div>`;
|
||||
};
|
||||
|
||||
Pattern.prototype.drawSizeValuesTo = function (sizeContainer) {
|
||||
sizeContainer.innerHTML += `<div><strong>Size (x, y):</strong> ${Math.round(
|
||||
this.right / 10
|
||||
)}mm x ${Math.round(this.bottom / 10)}mm </div>`;
|
||||
};
|
||||
|
||||
export { Pattern, Color, stitchTypes };
|
||||
107
src/format-readers/dst.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// @ts-nocheck
|
||||
import { stitchTypes } from "../file-renderer/pattern";
|
||||
|
||||
function decodeExp(b2) {
|
||||
let returnCode = 0;
|
||||
if (b2 === 0xf3) {
|
||||
return stitchTypes.end;
|
||||
}
|
||||
if ((b2 & 0xc3) === 0xc3) {
|
||||
return stitchTypes.trim | stitchTypes.stop;
|
||||
}
|
||||
if (b2 & 0x80) {
|
||||
returnCode |= stitchTypes.trim;
|
||||
}
|
||||
if (b2 & 0x40) {
|
||||
returnCode |= stitchTypes.stop;
|
||||
}
|
||||
return returnCode;
|
||||
}
|
||||
|
||||
export function dstRead(file, pattern) {
|
||||
let flags,
|
||||
x,
|
||||
y,
|
||||
prevJump = false,
|
||||
thisJump = false,
|
||||
b = [],
|
||||
byteCount = file.byteLength;
|
||||
file.seek(512);
|
||||
|
||||
while (file.tell() < byteCount - 3) {
|
||||
b[0] = file.getUint8();
|
||||
b[1] = file.getUint8();
|
||||
b[2] = file.getUint8();
|
||||
x = 0;
|
||||
y = 0;
|
||||
if (b[0] & 0x01) {
|
||||
x += 1;
|
||||
}
|
||||
if (b[0] & 0x02) {
|
||||
x -= 1;
|
||||
}
|
||||
if (b[0] & 0x04) {
|
||||
x += 9;
|
||||
}
|
||||
if (b[0] & 0x08) {
|
||||
x -= 9;
|
||||
}
|
||||
if (b[0] & 0x80) {
|
||||
y += 1;
|
||||
}
|
||||
if (b[0] & 0x40) {
|
||||
y -= 1;
|
||||
}
|
||||
if (b[0] & 0x20) {
|
||||
y += 9;
|
||||
}
|
||||
if (b[0] & 0x10) {
|
||||
y -= 9;
|
||||
}
|
||||
if (b[1] & 0x01) {
|
||||
x += 3;
|
||||
}
|
||||
if (b[1] & 0x02) {
|
||||
x -= 3;
|
||||
}
|
||||
if (b[1] & 0x04) {
|
||||
x += 27;
|
||||
}
|
||||
if (b[1] & 0x08) {
|
||||
x -= 27;
|
||||
}
|
||||
if (b[1] & 0x80) {
|
||||
y += 3;
|
||||
}
|
||||
if (b[1] & 0x40) {
|
||||
y -= 3;
|
||||
}
|
||||
if (b[1] & 0x20) {
|
||||
y += 27;
|
||||
}
|
||||
if (b[1] & 0x10) {
|
||||
y -= 27;
|
||||
}
|
||||
if (b[2] & 0x04) {
|
||||
x += 81;
|
||||
}
|
||||
if (b[2] & 0x08) {
|
||||
x -= 81;
|
||||
}
|
||||
if (b[2] & 0x20) {
|
||||
y += 81;
|
||||
}
|
||||
if (b[2] & 0x10) {
|
||||
y -= 81;
|
||||
}
|
||||
flags = decodeExp(b[2]);
|
||||
thisJump = flags & stitchTypes.jump;
|
||||
if (prevJump) {
|
||||
flags |= stitchTypes.jump;
|
||||
}
|
||||
pattern.addStitchRel(x, y, flags, true);
|
||||
prevJump = thisJump;
|
||||
}
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
50
src/format-readers/exp.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { stitchTypes } from "../file-renderer/pattern";
|
||||
|
||||
function expDecode(input) {
|
||||
return input > 128 ? -(~input & 255) - 1 : input;
|
||||
}
|
||||
|
||||
export function expRead(file, pattern) {
|
||||
let b0 = 0,
|
||||
b1 = 0,
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
flags = 0,
|
||||
i = 0,
|
||||
byteCount = file.byteLength;
|
||||
while (i < byteCount) {
|
||||
flags = stitchTypes.normal;
|
||||
b0 = file.getInt8(i);
|
||||
i += 1;
|
||||
b1 = file.getInt8(i);
|
||||
i += 1;
|
||||
if (b0 === -128) {
|
||||
if (b1 & 1) {
|
||||
b0 = file.getInt8(i);
|
||||
i += 1;
|
||||
b1 = file.getInt8(i);
|
||||
i += 1;
|
||||
flags = stitchTypes.stop;
|
||||
} else if (b1 === 2 || b1 === 4) {
|
||||
b0 = file.getInt8(i);
|
||||
i += 1;
|
||||
b1 = file.getInt8(i);
|
||||
i += 1;
|
||||
flags = stitchTypes.trim;
|
||||
} else if (b1 === -128) {
|
||||
b0 = file.getInt8(i);
|
||||
i += 1;
|
||||
b1 = file.getInt8(i);
|
||||
i += 1;
|
||||
b0 = 0;
|
||||
b1 = 0;
|
||||
flags = stitchTypes.trim;
|
||||
}
|
||||
}
|
||||
dx = expDecode(b0);
|
||||
dy = expDecode(b1);
|
||||
pattern.addStitchRel(dx, dy, flags, true);
|
||||
}
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end);
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
15
src/format-readers/index.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { dstRead } from "./dst";
|
||||
import { expRead } from "./exp";
|
||||
import { jefRead } from "./jef";
|
||||
import { pecRead } from "./pec";
|
||||
import { pesRead } from "./pes";
|
||||
|
||||
const supportedFormats = {
|
||||
pes: { ext: ".pes", read: pesRead },
|
||||
dst: { ext: ".dst", read: dstRead },
|
||||
pec: { ext: ".pec", read: pecRead },
|
||||
jef: { ext: ".jef", read: jefRead },
|
||||
exp: { ext: ".exp", read: expRead },
|
||||
};
|
||||
|
||||
export { supportedFormats };
|
||||
135
src/format-readers/jef.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { Color, stitchTypes } from "../file-renderer/pattern";
|
||||
|
||||
const colors = [
|
||||
new Color(0, 0, 0, "Black"),
|
||||
new Color(0, 0, 0, "Black"),
|
||||
new Color(255, 255, 255, "White"),
|
||||
new Color(255, 255, 23, "Yellow"),
|
||||
new Color(250, 160, 96, "Orange"),
|
||||
new Color(92, 118, 73, "Olive Green"),
|
||||
new Color(64, 192, 48, "Green"),
|
||||
new Color(101, 194, 200, "Sky"),
|
||||
new Color(172, 128, 190, "Purple"),
|
||||
new Color(245, 188, 203, "Pink"),
|
||||
new Color(255, 0, 0, "Red"),
|
||||
new Color(192, 128, 0, "Brown"),
|
||||
new Color(0, 0, 240, "Blue"),
|
||||
new Color(228, 195, 93, "Gold"),
|
||||
new Color(165, 42, 42, "Dark Brown"),
|
||||
new Color(213, 176, 212, "Pale Violet"),
|
||||
new Color(252, 242, 148, "Pale Yellow"),
|
||||
new Color(240, 208, 192, "Pale Pink"),
|
||||
new Color(255, 192, 0, "Peach"),
|
||||
new Color(201, 164, 128, "Beige"),
|
||||
new Color(155, 61, 75, "Wine Red"),
|
||||
new Color(160, 184, 204, "Pale Sky"),
|
||||
new Color(127, 194, 28, "Yellow Green"),
|
||||
new Color(185, 185, 185, "Silver Grey"),
|
||||
new Color(160, 160, 160, "Grey"),
|
||||
new Color(152, 214, 189, "Pale Aqua"),
|
||||
new Color(184, 240, 240, "Baby Blue"),
|
||||
new Color(54, 139, 160, "Powder Blue"),
|
||||
new Color(79, 131, 171, "Bright Blue"),
|
||||
new Color(56, 106, 145, "Slate Blue"),
|
||||
new Color(0, 32, 107, "Nave Blue"),
|
||||
new Color(229, 197, 202, "Salmon Pink"),
|
||||
new Color(249, 103, 107, "Coral"),
|
||||
new Color(227, 49, 31, "Burnt Orange"),
|
||||
new Color(226, 161, 136, "Cinnamon"),
|
||||
new Color(181, 148, 116, "Umber"),
|
||||
new Color(228, 207, 153, "Blonde"),
|
||||
new Color(225, 203, 0, "Sunflower"),
|
||||
new Color(225, 173, 212, "Orchid Pink"),
|
||||
new Color(195, 0, 126, "Peony Purple"),
|
||||
new Color(128, 0, 75, "Burgundy"),
|
||||
new Color(160, 96, 176, "Royal Purple"),
|
||||
new Color(192, 64, 32, "Cardinal Red"),
|
||||
new Color(202, 224, 192, "Opal Green"),
|
||||
new Color(137, 152, 86, "Moss Green"),
|
||||
new Color(0, 170, 0, "Meadow Green"),
|
||||
new Color(33, 138, 33, "Dark Green"),
|
||||
new Color(93, 174, 148, "Aquamarine"),
|
||||
new Color(76, 191, 143, "Emerald Green"),
|
||||
new Color(0, 119, 114, "Peacock Green"),
|
||||
new Color(112, 112, 112, "Dark Grey"),
|
||||
new Color(242, 255, 255, "Ivory White"),
|
||||
new Color(177, 88, 24, "Hazel"),
|
||||
new Color(203, 138, 7, "Toast"),
|
||||
new Color(247, 146, 123, "Salmon"),
|
||||
new Color(152, 105, 45, "Cocoa Brown"),
|
||||
new Color(162, 113, 72, "Sienna"),
|
||||
new Color(123, 85, 74, "Sepia"),
|
||||
new Color(79, 57, 70, "Dark Sepia"),
|
||||
new Color(82, 58, 151, "Violet Blue"),
|
||||
new Color(0, 0, 160, "Blue Ink"),
|
||||
new Color(0, 150, 222, "Solar Blue"),
|
||||
new Color(178, 221, 83, "Green Dust"),
|
||||
new Color(250, 143, 187, "Crimson"),
|
||||
new Color(222, 100, 158, "Floral Pink"),
|
||||
new Color(181, 80, 102, "Wine"),
|
||||
new Color(94, 87, 71, "Olive Drab"),
|
||||
new Color(76, 136, 31, "Meadow"),
|
||||
new Color(228, 220, 121, "Mustard"),
|
||||
new Color(203, 138, 26, "Yellow Ochre"),
|
||||
new Color(198, 170, 66, "Old Gold"),
|
||||
new Color(236, 176, 44, "Honeydew"),
|
||||
new Color(248, 128, 64, "Tangerine"),
|
||||
new Color(255, 229, 5, "Canary Yellow"),
|
||||
new Color(250, 122, 122, "Vermillion"),
|
||||
new Color(107, 224, 0, "Bright Green"),
|
||||
new Color(56, 108, 174, "Ocean Blue"),
|
||||
new Color(227, 196, 180, "Beige Grey"),
|
||||
new Color(227, 172, 129, "Bamboo"),
|
||||
];
|
||||
|
||||
function jefDecode(inputByte) {
|
||||
return inputByte >= 0x80 ? -(~inputByte & 0xff) - 1 : inputByte;
|
||||
}
|
||||
|
||||
export function jefRead(file, pattern) {
|
||||
file.seek(24);
|
||||
const numberOfColors = file.getInt32(file.tell(), true);
|
||||
const numberOfStitches = file.getInt32(file.tell(), true);
|
||||
file.seek(file.tell() + 84);
|
||||
|
||||
for (let i = 0; i < numberOfColors; i += 1) {
|
||||
pattern.addColor(colors[file.getUint32(file.tell(), true) % 78]);
|
||||
}
|
||||
for (let i = 0; i < 6 - numberOfColors; i += 1) {
|
||||
file.getUint32();
|
||||
}
|
||||
|
||||
let flags,
|
||||
b0,
|
||||
b1,
|
||||
dx,
|
||||
dy,
|
||||
stitchCount = 0;
|
||||
while (stitchCount < numberOfStitches + 100) {
|
||||
flags = stitchTypes.normal;
|
||||
b0 = file.getUint8();
|
||||
b1 = file.getUint8();
|
||||
|
||||
if (b0 === 0x80) {
|
||||
if (b1 & 0x01) {
|
||||
b0 = file.getUint8();
|
||||
b1 = file.getUint8();
|
||||
flags = stitchTypes.stop;
|
||||
} else if (b1 === 0x02 || b1 === 0x04) {
|
||||
b0 = file.getUint8();
|
||||
b1 = file.getUint8();
|
||||
flags = stitchTypes.trim;
|
||||
} else if (b1 === 0x10) {
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
dx = jefDecode(b0);
|
||||
dy = jefDecode(b1);
|
||||
pattern.addStitchRel(dx, dy, flags, true);
|
||||
stitchCount += 1;
|
||||
}
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
|
||||
export const jefColors = colors;
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
import { pecColors, pecReadStitches } from './pes';
|
||||
import { pecColors, pecReadStitches } from "./pes";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern
|
||||
*/
|
||||
export function pecRead(file, pattern) {
|
||||
let colorChanges, i;
|
||||
file.seek(0x38);
|
||||
153
src/format-readers/pes.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { Color, stitchTypes } from "../file-renderer/pattern";
|
||||
|
||||
const namedColors = [
|
||||
new Color(0, 0, 0, "Unknown"),
|
||||
new Color(14, 31, 124, "Prussian Blue"),
|
||||
new Color(10, 85, 163, "Blue"),
|
||||
new Color(0, 135, 119, "Teal Green"),
|
||||
new Color(75, 107, 175, "Cornflower Blue"),
|
||||
new Color(237, 23, 31, "Red"),
|
||||
new Color(209, 92, 0, "Reddish Brown"),
|
||||
new Color(145, 54, 151, "Magenta"),
|
||||
new Color(228, 154, 203, "Light Lilac"),
|
||||
new Color(145, 95, 172, "Lilac"),
|
||||
new Color(158, 214, 125, "Mint Green"),
|
||||
new Color(232, 169, 0, "Deep Gold"),
|
||||
new Color(254, 186, 53, "Orange"),
|
||||
new Color(255, 255, 0, "Yellow"),
|
||||
new Color(112, 188, 31, "Lime Green"),
|
||||
new Color(186, 152, 0, "Brass"),
|
||||
new Color(168, 168, 168, "Silver"),
|
||||
new Color(125, 111, 0, "Russet Brown"),
|
||||
new Color(255, 255, 179, "Cream Brown"),
|
||||
new Color(79, 85, 86, "Pewter"),
|
||||
new Color(0, 0, 0, "Black"),
|
||||
new Color(11, 61, 145, "Ultramarine"),
|
||||
new Color(119, 1, 118, "Royal Purple"),
|
||||
new Color(41, 49, 51, "Dark Gray"),
|
||||
new Color(42, 19, 1, "Dark Brown"),
|
||||
new Color(246, 74, 138, "Deep Rose"),
|
||||
new Color(178, 118, 36, "Light Brown"),
|
||||
new Color(252, 187, 197, "Salmon Pink"),
|
||||
new Color(254, 55, 15, "Vermillion"),
|
||||
new Color(240, 240, 240, "White"),
|
||||
new Color(106, 28, 138, "Violet"),
|
||||
new Color(168, 221, 196, "Seacrest"),
|
||||
new Color(37, 132, 187, "Sky Blue"),
|
||||
new Color(254, 179, 67, "Pumpkin"),
|
||||
new Color(255, 243, 107, "Cream Yellow"),
|
||||
new Color(208, 166, 96, "Khaki"),
|
||||
new Color(209, 84, 0, "Clay Brown"),
|
||||
new Color(102, 186, 73, "Leaf Green"),
|
||||
new Color(19, 74, 70, "Peacock Blue"),
|
||||
new Color(135, 135, 135, "Gray"),
|
||||
new Color(216, 204, 198, "Warm Gray"),
|
||||
new Color(67, 86, 7, "Dark Olive"),
|
||||
new Color(253, 217, 222, "Flesh Pink"),
|
||||
new Color(249, 147, 188, "Pink"),
|
||||
new Color(0, 56, 34, "Deep Green"),
|
||||
new Color(178, 175, 212, "Lavender"),
|
||||
new Color(104, 106, 176, "Wisteria Violet"),
|
||||
new Color(239, 227, 185, "Beige"),
|
||||
new Color(247, 56, 102, "Carmine"),
|
||||
new Color(181, 75, 100, "Amber Red"),
|
||||
new Color(19, 43, 26, "Olive Green"),
|
||||
new Color(199, 1, 86, "Dark Fuschia"),
|
||||
new Color(254, 158, 50, "Tangerine"),
|
||||
new Color(168, 222, 235, "Light Blue"),
|
||||
new Color(0, 103, 62, "Emerald Green"),
|
||||
new Color(78, 41, 144, "Purple"),
|
||||
new Color(47, 126, 32, "Moss Green"),
|
||||
new Color(255, 204, 204, "Flesh Pink"),
|
||||
new Color(255, 217, 17, "Harvest Gold"),
|
||||
new Color(9, 91, 166, "Electric Blue"),
|
||||
new Color(240, 249, 112, "Lemon Yellow"),
|
||||
new Color(227, 243, 91, "Fresh Green"),
|
||||
new Color(255, 153, 0, "Orange"),
|
||||
new Color(255, 240, 141, "Cream Yellow"),
|
||||
new Color(255, 200, 200, "Applique"),
|
||||
];
|
||||
|
||||
function readPecStitches(file, pattern) {
|
||||
let stitchNumber = 0;
|
||||
const byteCount = file.byteLength;
|
||||
|
||||
while (file.tell() < byteCount) {
|
||||
let [xOffset, yOffset] = [file.getUint8(), file.getUint8()];
|
||||
let stitchType = stitchTypes.normal;
|
||||
|
||||
if (isEndStitch(xOffset, yOffset)) {
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isStopStitch(xOffset, yOffset)) {
|
||||
file.getInt8(); // Skip extra byte
|
||||
pattern.addStitchRel(0, 0, stitchTypes.stop, true);
|
||||
stitchNumber++;
|
||||
continue;
|
||||
}
|
||||
|
||||
stitchType = determineStitchType(xOffset, yOffset);
|
||||
[xOffset, yOffset] = decodeCoordinates(xOffset, yOffset, file);
|
||||
|
||||
pattern.addStitchRel(xOffset, yOffset, stitchType, true);
|
||||
stitchNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
function isEndStitch(xOffset, yOffset) {
|
||||
return xOffset === 0xff && yOffset === 0x00;
|
||||
}
|
||||
|
||||
function isStopStitch(xOffset, yOffset) {
|
||||
return xOffset === 0xfe && yOffset === 0xb0;
|
||||
}
|
||||
|
||||
function determineStitchType(xOffset, yOffset) {
|
||||
if (xOffset & 0x80) {
|
||||
if (xOffset & 0x20) return stitchTypes.trim;
|
||||
if (xOffset & 0x10) return stitchTypes.jump;
|
||||
}
|
||||
if (yOffset & 0x80) {
|
||||
if (yOffset & 0x20) return stitchTypes.trim;
|
||||
if (yOffset & 0x10) return stitchTypes.jump;
|
||||
}
|
||||
return stitchTypes.normal;
|
||||
}
|
||||
|
||||
function decodeCoordinates(xOffset, yOffset, file) {
|
||||
if (xOffset & 0x80) {
|
||||
xOffset = ((xOffset & 0x0f) << 8) + yOffset;
|
||||
if (xOffset & 0x800) xOffset -= 0x1000;
|
||||
yOffset = file.getUint8();
|
||||
} else if (xOffset >= 0x40) {
|
||||
xOffset -= 0x80;
|
||||
}
|
||||
|
||||
if (yOffset & 0x80) {
|
||||
yOffset = ((yOffset & 0x0f) << 8) + file.getUint8();
|
||||
if (yOffset & 0x800) yOffset -= 0x1000;
|
||||
} else if (yOffset > 0x3f) {
|
||||
yOffset -= 0x80;
|
||||
}
|
||||
|
||||
return [xOffset, yOffset];
|
||||
}
|
||||
|
||||
export function pesRead(file, pattern) {
|
||||
const pecStart = file.getInt32(8, true);
|
||||
file.seek(pecStart + 48);
|
||||
|
||||
const numColors = file.getInt8() + 1;
|
||||
for (let i = 0; i < numColors; i++) {
|
||||
pattern.addColor(namedColors[file.getInt8()]);
|
||||
}
|
||||
|
||||
file.seek(pecStart + 532);
|
||||
readPecStitches(file, pattern);
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end);
|
||||
}
|
||||
|
||||
export const pecReadStitches = readPecStitches;
|
||||
export const pecColors = namedColors;
|
||||
|
|
@ -1,72 +1,47 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
import renderFileToCanvas from '$lib/file-renderer';
|
||||
import renderFileToCanvas from "../file-renderer";
|
||||
|
||||
/**
|
||||
* @type {ArrayLike<any>}
|
||||
*/
|
||||
export let files = [];
|
||||
|
||||
/**
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
let errorMessageRef;
|
||||
let canvasRefs = [];
|
||||
let colorRefs = [];
|
||||
let stitchesRefs = [];
|
||||
let sizeRefs = [];
|
||||
let localizedStrings = {
|
||||
stitches: $t('viewer.stitches'),
|
||||
dimensions: $t('viewer.dimensions'),
|
||||
};
|
||||
let errorMessageRef;
|
||||
|
||||
/**
|
||||
* Downloads a given HTMLCanvasElement as a PNG image.
|
||||
*
|
||||
* @param {HTMLCanvasElement} canvas - The canvas element to export as an image.
|
||||
* @param {string} filename - The desired name of the downloaded file (extension will be replaced with `.png`).
|
||||
*/
|
||||
const downloadCanvasAsImage = (canvas, filename) => {
|
||||
const image = canvas
|
||||
.toDataURL('image/png')
|
||||
.replace('image/png', 'image/octet-stream');
|
||||
.toDataURL("image/png")
|
||||
.replace("image/png", "image/octet-stream");
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.download = `${filename.split('.').slice(0, -1).join('.')}.png`;
|
||||
const link = document.createElement("a");
|
||||
link.download = `${filename.split(".").slice(0, -1).join(".")}.png`;
|
||||
link.href = image;
|
||||
link.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Cliks the button to render the files when user press enter.
|
||||
*
|
||||
* @param {KeyboardEvent} evt - The event that triggered the language switch.
|
||||
*/
|
||||
const onKeydown = (evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
const button = document.getElementById('download-button');
|
||||
if (button) button.click();
|
||||
if (evt.key === "Enter") {
|
||||
document.getElementById("download-button").click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if files.length !== 0}
|
||||
<div id="container" style="width: 100%; heigth: 100vh;">
|
||||
{#each Array.from(files) as file, i (i)}
|
||||
{#each Array.from(files) as file, i}
|
||||
<div class="canvas-container">
|
||||
<canvas bind:this={canvasRefs[i]} class="canvas"></canvas>
|
||||
<canvas bind:this={canvasRefs[i]} class="canvas" />
|
||||
<p><strong>{file.name}</strong></p>
|
||||
<div class="stitches-container" bind:this={stitchesRefs[i]}></div>
|
||||
<div class="size-container" bind:this={sizeRefs[i]}></div>
|
||||
<div class="colors-container" bind:this={colorRefs[i]}></div>
|
||||
<div class="stitches-container" bind:this={stitchesRefs[i]} />
|
||||
<div class="size-container" bind:this={sizeRefs[i]} />
|
||||
<div class="colors-container" bind:this={colorRefs[i]} />
|
||||
<div
|
||||
id="download-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={onKeydown}
|
||||
onclick={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
|
||||
on:keydown={onKeydown}
|
||||
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
|
||||
>
|
||||
{$t('viewer.download')}
|
||||
Download
|
||||
</div>
|
||||
</div>
|
||||
{canvasRefs[i] &&
|
||||
|
|
@ -76,12 +51,11 @@
|
|||
errorMessageRef,
|
||||
colorRefs[i],
|
||||
stitchesRefs[i],
|
||||
sizeRefs[i],
|
||||
localizedStrings,
|
||||
sizeRefs[i]
|
||||
)}
|
||||
{/each}
|
||||
<!-- svelte-ignore a11y-missing-content -->
|
||||
<h1 bind:this={errorMessageRef}></h1>
|
||||
<h1 bind:this={errorMessageRef} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -103,9 +77,7 @@
|
|||
max-height: 1000px;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
/* border: 2px solid black;*/
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 16px;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
|
|
@ -127,18 +99,15 @@
|
|||
padding: 10px 0;
|
||||
}
|
||||
|
||||
div[role='button'] {
|
||||
div[role="button"] {
|
||||
background-color: #05345f;
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
div[role='button']:hover {
|
||||
div[role="button"]:hover {
|
||||
cursor: pointer;
|
||||
background-color: black;
|
||||
color: white;
|
||||
|
|
@ -153,10 +122,5 @@
|
|||
#container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div[role='button'] {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
src/lib/Dropzone.svelte
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
export let files;
|
||||
export let supportedFormats;
|
||||
export let onKeydown;
|
||||
export let onClick;
|
||||
export let onDrop;
|
||||
export let onChange;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div
|
||||
id="dropzone"
|
||||
tabindex={0}
|
||||
on:keydown={onKeydown}
|
||||
on:click={onClick}
|
||||
on:dragover|preventDefault|stopPropagation
|
||||
on:drop|preventDefault|stopPropagation={onDrop}
|
||||
>
|
||||
<label id="file-label" for="file-input"
|
||||
>Drag and drop files here or click to upload.</label
|
||||
>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
name="files[]"
|
||||
accept={supportedFormats.join(",")}
|
||||
multiple
|
||||
on:change={onChange}
|
||||
bind:files
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#dropzone {
|
||||
display: flex;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
border: 5px dotted black;
|
||||
padding: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#file-label {
|
||||
z-index: -1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#dropzone:hover {
|
||||
cursor: pointer;
|
||||
border: 5px dotted #05345f;
|
||||
color: #05345f;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
#dropzone {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
44
src/lib/FileList.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script>
|
||||
export let title;
|
||||
export let files = [];
|
||||
export let isError = false;
|
||||
</script>
|
||||
|
||||
{#if files.length !== 0}
|
||||
<div id="selected-files-container">
|
||||
<h2>{title}:</h2>
|
||||
{#each Array.from(files) as file}
|
||||
<div id={isError ? "selected-file-card-error" : "selected-file-card"}>
|
||||
<p>{file.name} ({file.size / 1000} kb)</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#selected-file-card {
|
||||
border: 1px solid #000;
|
||||
width: 500px;
|
||||
padding-left: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#selected-file-card-error {
|
||||
border: 1px solid red;
|
||||
width: 500px;
|
||||
padding-left: 15px;
|
||||
margin-top: 10px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
#selected-files-container,
|
||||
#selected-file-card-error {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#selected-file-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/lib/FileViewer.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import CardList from "./CardList.svelte";
|
||||
import Dropzone from "./Dropzone.svelte";
|
||||
import FileList from "./FileList.svelte";
|
||||
|
||||
import { filterFiles } from "../utils/filterFiles";
|
||||
import { supportedFormats } from "../format-readers";
|
||||
|
||||
let acceptedFiles;
|
||||
let rejectedFiles;
|
||||
let areAcceptedFilesRendered = false;
|
||||
const fileRequirements = {
|
||||
supportedFormats: Object.values(supportedFormats).map((f) => f.ext),
|
||||
maxSize: 700000,
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
areAcceptedFilesRendered = true;
|
||||
};
|
||||
|
||||
const onDrop = (evt) => {
|
||||
onChange(evt);
|
||||
};
|
||||
|
||||
const onChange = (evt) => {
|
||||
acceptedFiles = null;
|
||||
areAcceptedFilesRendered = false;
|
||||
|
||||
const changedFiles = evt.dataTransfer
|
||||
? evt.dataTransfer.files
|
||||
: evt.target.files;
|
||||
|
||||
const results = filterFiles(changedFiles, fileRequirements);
|
||||
acceptedFiles = results.accepted;
|
||||
rejectedFiles = results.rejected;
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
document.getElementById("file-input").click();
|
||||
};
|
||||
|
||||
const onKeydown = (evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
document.getElementById("file-input").click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
id="form"
|
||||
enctype="multipart/form-data"
|
||||
on:submit|preventDefault|stopPropagation={onSubmit}
|
||||
>
|
||||
<h2>Upload files</h2>
|
||||
<p>
|
||||
Max file size is <strong>{fileRequirements.maxSize / 1000}kb</strong>.
|
||||
Accepted formats:
|
||||
<strong>{fileRequirements.supportedFormats.join(", ")}</strong>.
|
||||
</p>
|
||||
|
||||
<Dropzone
|
||||
files={acceptedFiles}
|
||||
supportedFormats={fileRequirements.supportedFormats}
|
||||
{onKeydown}
|
||||
{onClick}
|
||||
{onDrop}
|
||||
{onChange}
|
||||
/>
|
||||
|
||||
<input type="submit" value="Render files" />
|
||||
</form>
|
||||
|
||||
{#if areAcceptedFilesRendered}
|
||||
<CardList files={acceptedFiles} />
|
||||
{:else}
|
||||
<FileList title="Rejected Files" files={rejectedFiles} isError />
|
||||
<FileList title="Selected Files" files={acceptedFiles} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
#form {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/lib/Footer.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import { appVersion } from "../utils/env";
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
Copyright © {new Date().getFullYear()}
|
||||
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
|
||||
>Leonardo Murça</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
version: {appVersion()}
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
bottom: 0;
|
||||
}
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
42
src/lib/Header.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import MediaQuery from "../lib/MediaQuery.svelte";
|
||||
import logo from "../assets/embroidery-viewer-logo.webp";
|
||||
import logoMobile from "../assets/embroidery-viewer-logo-mobile.webp";
|
||||
|
||||
const configsFor = (matches) => {
|
||||
return matches
|
||||
? { src: logoMobile, width: 350, height: 96 }
|
||||
: { src: logo, width: 460, height: 200 };
|
||||
};
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<a href="/">
|
||||
<MediaQuery query="(min-width: 481px) and (max-width: 812px)" let:matches>
|
||||
{@const configs = configsFor(matches)}
|
||||
<img
|
||||
class="logo"
|
||||
alt="Embroidery viewer logo."
|
||||
src={configs.src}
|
||||
width={configs.width}
|
||||
height={configs.height}
|
||||
/>
|
||||
</MediaQuery>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
margin-top: 100px;
|
||||
}
|
||||
.logo {
|
||||
background-image: logo;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
.logo {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,9 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let query;
|
||||
|
||||
/**
|
||||
* @type {MediaQueryList}
|
||||
*/
|
||||
let mql;
|
||||
/**
|
||||
* @type {((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null}
|
||||
*/
|
||||
let mqlListener;
|
||||
let wasMounted = false;
|
||||
let matches = false;
|
||||
|
|
@ -28,9 +22,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} query
|
||||
*/
|
||||
function addNewListener(query) {
|
||||
mql = window.matchMedia(query);
|
||||
mqlListener = (v) => (matches = v.matches);
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="68.965652mm"
|
||||
height="68.948975mm"
|
||||
viewBox="0 0 68.965652 68.948975"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(-78.581248,-114.14203)"><path
|
||||
d="m 101.71922,133.815 7.89657,-7.93103 v 33.06897 c 0,4.59771 6.89657,4.59771 6.89657,0 v -33.06897 l 7.89657,7.93103 c 1.34889,1.35999 3.54767,1.35999 4.89656,0 1.36001,-1.34889 1.36001,-3.5477 0,-4.89659 l -13.79313,-13.79308 c -0.32789,-0.31411 -0.71459,-0.56036 -1.1379,-0.72463 -0.83953,-0.34489 -1.78117,-0.34489 -2.6207,0 -0.42331,0.16427 -0.81001,0.41052 -1.1379,0.72463 l -13.793128,13.79308 c -3.265094,3.26437 1.631464,8.16096 4.896558,4.89659 z m 42.3794,14.7931 c -1.90447,0 -3.44833,1.5439 -3.44828,3.44837 v 20.68969 c -2e-5,1.90442 -1.54386,3.44824 -3.44828,3.44824 H 88.926098 c -1.904415,0 -3.448254,-1.54382 -3.448278,-3.44824 v -20.68969 c 0,-4.59771 -6.89657,-4.59771 -6.89657,0 v 20.68969 c -10e-7,5.7133 4.631545,10.34485 10.344848,10.34485 h 48.275962 c 5.7133,0 10.34485,-4.63155 10.34485,-10.34485 v -20.68969 c 5e-5,-1.90447 -1.54382,-3.44837 -3.44829,-3.44837 z"
|
||||
id="path1"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:#06345f;fill-opacity:1;stroke-width:3.448;stroke-dasharray:none" /></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,9 +0,0 @@
|
|||
<script>
|
||||
import { isDevelopment } from '$lib/utils/env';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if !isDevelopment()}
|
||||
<script async src="https://hk.leomurca.xyz/hk.js"></script>
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
const trackEvent = () => {
|
||||
// @ts-ignore
|
||||
window.hk?.event?.('install_now');
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="bar">
|
||||
<div class="content">
|
||||
<p>
|
||||
{$t('announcement.message')}
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=xyz.embroideryviewer.android"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="cta"
|
||||
onclick={trackEvent}
|
||||
>
|
||||
{$t('announcement.cta-text')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
width: 100%;
|
||||
background: #06345f;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0.6rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cta {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cta:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
import upload from '$lib/assets/upload.svg';
|
||||
|
||||
export let files;
|
||||
export let supportedFormats;
|
||||
export let onKeydown;
|
||||
export let onClick;
|
||||
export let onDrop;
|
||||
export let onChange;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- eslint-disable svelte/no-at-html-tags -->
|
||||
<div
|
||||
id="dropzone"
|
||||
tabindex={0}
|
||||
role="region"
|
||||
on:keydown={onKeydown}
|
||||
on:dragover|preventDefault|stopPropagation
|
||||
on:drop|preventDefault|stopPropagation={onDrop}
|
||||
>
|
||||
<img src={upload} width="40" height="40" alt="Upload icon" />
|
||||
<label id="file-label" for="file-input">{@html $t('viewer.dropzone')}</label>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
name="files[]"
|
||||
accept={supportedFormats.join(',')}
|
||||
multiple
|
||||
on:change={onChange}
|
||||
bind:this={files}
|
||||
/>
|
||||
<button on:click|preventDefault={onClick}>{$t('viewer.browse')}</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border: 1px solid #d3dce6;
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#file-label {
|
||||
z-index: -1;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background-color: #06345f;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
width: 60%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
#dropzone {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<script>
|
||||
export let title;
|
||||
/**
|
||||
* @type {ArrayLike<any>}
|
||||
*/
|
||||
export let files = [];
|
||||
export let isError = false;
|
||||
</script>
|
||||
|
||||
{#if files.length !== 0}
|
||||
<div id="selected-files-container">
|
||||
<h2>{title}:</h2>
|
||||
<div id="files-list">
|
||||
{#each Array.from(files) as file, i (i)}
|
||||
<div id={isError ? 'selected-file-card-error' : 'selected-file-card'}>
|
||||
<span>{file.name}</span>
|
||||
<span>{Math.round(file.size / 1000)} KB</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#selected-files-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#selected-file-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #06345f;
|
||||
font-weight: bolder;
|
||||
width: 500px;
|
||||
padding-left: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#selected-file-card-error {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #06345f;
|
||||
font-weight: bolder;
|
||||
width: 500px;
|
||||
padding-left: 15px;
|
||||
margin-top: 10px;
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 812px) {
|
||||
#selected-files-container,
|
||||
#selected-file-card-error {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#selected-file-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { t } from '$lib/translations';
|
||||
import { appVersion } from '$lib/utils/env';
|
||||
|
||||
import MailIcon from '$lib/components/icons/MailIcon.svelte';
|
||||
import ArrowTopIcon from './icons/ArrowTopIcon.svelte';
|
||||
import logo from '$lib/assets/logo-white.webp';
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<div id="content-container">
|
||||
<section class="footer-block">
|
||||
<img
|
||||
src={logo}
|
||||
style="height: 80px; width: auto; margin-lft: -5px;"
|
||||
alt="Logotipo da Embroidery Viewer."
|
||||
/>
|
||||
<p>{$t('footer.slogan')}</p>
|
||||
</section>
|
||||
<section class="footer-block" aria-labelledby="contact-title">
|
||||
<h1 id="contact-title">{$t('footer.contact-title')}</h1>
|
||||
<p>{$t('footer.contact-description')}</p>
|
||||
|
||||
<address class="contact-container">
|
||||
<div class="contact-item">
|
||||
<MailIcon size={30} />
|
||||
|
||||
<a href="mailto:leo@leomurca.xyz}" aria-label="leo@leomurca.xyz"
|
||||
>leo@leomurca.xyz</a
|
||||
>
|
||||
</div>
|
||||
</address>
|
||||
</section>
|
||||
<section class="footer-block">
|
||||
<h1>{$t('footer.resources')}</h1>
|
||||
<nav class="social-container" aria-label="Social media">
|
||||
<a href={resolve('/about')}>{$t('footer.about')}</a>
|
||||
<a href={resolve('/privacy-policy')}>{$t('footer.privacy.policy')}</a>
|
||||
<a href={resolve('/terms-of-service')}
|
||||
>{$t('footer.terms.of.service')}</a
|
||||
>
|
||||
</nav>
|
||||
<button
|
||||
class="back-to-top-button"
|
||||
aria-label={$t('footer.back-to-top.aria-label')}
|
||||
onclick={() => scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
<ArrowTopIcon size={30} /> {$t('footer.back-to-top.label')}</button
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<section class="credits-container">
|
||||
Copyright {new Date().getFullYear()}
|
||||
|
||||
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
|
||||
>Leonardo Murça</a
|
||||
>
|
||||
|
|
||||
{$t('footer.version')}:
|
||||
<span style="font-family: var(--font-bold);">{appVersion()}</span>
|
||||
</section>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
background-color: var(--color-primary);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content-container {
|
||||
width: 85%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
padding: 60px 0 60px 30px;
|
||||
scroll-margin-top: 100px;
|
||||
color: white;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.footer-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.contact-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
letter-spacing: 0.1rem;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.contact-container a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.contact-item:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.social-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.social-container a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social-container a:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.back-to-top-button {
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
padding: 13px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
width: 40%;
|
||||
margin-top: 30px;
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-to-top-button:hover {
|
||||
background-color: #ffffff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.credits-container {
|
||||
background-color: var(--color-secondary);
|
||||
color: white;
|
||||
margin: 0 auto;
|
||||
padding: 20px 30px;
|
||||
padding-left: 9%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.credits-container a {
|
||||
color: white;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
.credits-container a:hover {
|
||||
background-color: white;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#content-container {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 60px 30px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.back-to-top-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
<script>
|
||||
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
||||
import { locale, t } from '$lib/translations';
|
||||
import { normalizeLocaleUnderscore } from '$lib/utils/normalizeLocaleUnderscore';
|
||||
import { isDevelopment } from '$lib/utils/env';
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Props
|
||||
* =========================
|
||||
*/
|
||||
|
||||
/** @type {string} Page title (translation key) */
|
||||
export let title;
|
||||
|
||||
/** @type {string} Page description (translation key) */
|
||||
export let description;
|
||||
|
||||
/** @type {string} SEO keywords (translation key or raw string) */
|
||||
export let keywords;
|
||||
|
||||
/** @type {string} Canonical URL (absolute) */
|
||||
export let url;
|
||||
|
||||
/** @type {string} Open Graph type (e.g., 'website', 'article') */
|
||||
export let ogType = 'website';
|
||||
|
||||
/** @type {string} Optional override for Open Graph description */
|
||||
export let ogDescription = description;
|
||||
|
||||
/** @type {string} Twitter card type */
|
||||
export let twitterCard = 'summary_large_image';
|
||||
|
||||
/** @type {boolean} Whether the page should be indexed */
|
||||
export let shouldBeIndexed = !isDevelopment();
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* Derived / Computed values
|
||||
* =========================
|
||||
*/
|
||||
|
||||
let image = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
|
||||
|
||||
// Translations (avoid repeating $t everywhere)
|
||||
$: translatedTitle = title ? $t(title) : '';
|
||||
$: translatedDescription = description ? $t(description) : '';
|
||||
$: translatedKeywords = keywords ? $t(keywords) : '';
|
||||
|
||||
// Fallbacks
|
||||
$: finalOgDescription = ogDescription
|
||||
? $t(ogDescription)
|
||||
: translatedDescription;
|
||||
|
||||
// Locale formatting (e.g., en-US -> en_US)
|
||||
$: ogLocale = normalizeLocaleUnderscore($locale);
|
||||
|
||||
// Robots directive
|
||||
$: robotsContent = shouldBeIndexed ? 'index, follow' : 'noindex, nofollow';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<!-- Primary SEO -->
|
||||
<title>{translatedTitle}</title>
|
||||
<meta name="description" content={translatedDescription} />
|
||||
<meta name="keywords" content={translatedKeywords} />
|
||||
|
||||
<!-- Robots -->
|
||||
<meta name="robots" content={robotsContent} />
|
||||
<meta name="googlebot" content={robotsContent} />
|
||||
|
||||
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:title" content={translatedTitle} />
|
||||
<meta property="og:description" content={finalOgDescription} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:locale" content={ogLocale} />
|
||||
<meta property="og:site_name" content="Embroidery Viewer" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content={twitterCard} />
|
||||
<meta name="twitter:title" content={translatedTitle} />
|
||||
<meta name="twitter:description" content={finalOgDescription} />
|
||||
<meta name="twitter:image" content={image} />
|
||||
|
||||
<!-- Optional: improves link previews in some platforms -->
|
||||
<meta property="og:image:alt" content={translatedTitle} />
|
||||
</svelte:head>
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import logo from '$lib/assets/logo.webp';
|
||||
import LanguageSwitch from './LanguageSwitch.svelte';
|
||||
import MediaQuery from './MediaQuery.svelte';
|
||||
|
||||
let isOpen = $state(false);
|
||||
let route = $derived(page.url.pathname);
|
||||
$effect(() => {
|
||||
route; // track dependency
|
||||
isOpen = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="logo">
|
||||
<MediaQuery query="(max-width: 768px)" let:matches>
|
||||
<a href={resolve('/')}>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Embroidery viewer logo"
|
||||
width="150"
|
||||
height={matches ? 70 : 100}
|
||||
/>
|
||||
</a>
|
||||
</MediaQuery>
|
||||
</div>
|
||||
|
||||
<nav class:active={route !== '/'} id="menuToggle">
|
||||
<input type="checkbox" bind:checked={isOpen} />
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
|
||||
<ul id="menu">
|
||||
<li><a href={resolve('/about')}>{$t('header.aboutNav')}</a></li>
|
||||
<li><a href={resolve('/viewer')}>{$t('header.viewerNav')}</a></li>
|
||||
<li><a href={resolve('/support-us')}>{$t('header.supportUsNav')}</a></li>
|
||||
<li style="font-size: 22px; padding: 10px 0;"><LanguageSwitch /></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 45px 30px 10px 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
height: auto;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.logo a:hover {
|
||||
background: transparent;
|
||||
}
|
||||
#menuToggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* Click area */
|
||||
#menuToggle input {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -5px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Bars */
|
||||
#menuToggle span {
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
margin-bottom: 7px;
|
||||
background: #ffffff;
|
||||
border-radius: 2px;
|
||||
transition: all 0.35s ease;
|
||||
transform-origin: 4px 0px;
|
||||
}
|
||||
|
||||
/* Adjust origins for rotation */
|
||||
#menuToggle span:nth-child(2) {
|
||||
transform-origin: 0% 0%;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#menuToggle span:nth-child(4) {
|
||||
transform-origin: 0% 100%;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
/* === ANIMATION === */
|
||||
|
||||
/* Top bar → rotates */
|
||||
#menuToggle input:checked ~ span:nth-child(2) {
|
||||
transform: rotate(45deg) translate(-1px, -1px);
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
/* Middle bar → fades out */
|
||||
#menuToggle input:checked ~ span:nth-child(3) {
|
||||
opacity: 0;
|
||||
transform: scale(0.2);
|
||||
}
|
||||
|
||||
/* Bottom bar → rotates opposite */
|
||||
#menuToggle input:checked ~ span:nth-child(4) {
|
||||
transform: rotate(-45deg) translate(1px, -1px);
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
width: 400px;
|
||||
margin: -100px 0 0 0;
|
||||
padding: 50px;
|
||||
padding-top: 125px;
|
||||
right: -100px;
|
||||
background: var(--color-primary);
|
||||
list-style-type: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transform-origin: 0% 0%;
|
||||
transform: translate(100%, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
|
||||
z-index: -1;
|
||||
|
||||
margin: 30px;
|
||||
border-radius: 70% 30% 30% 70% / 60% 40% 0 100%;
|
||||
box-shadow:
|
||||
0 2.8px 2.2px rgba(0, 0, 0, 0.02),
|
||||
0 6.7px 5.3px rgba(0, 0, 0, 0.028),
|
||||
0 12.5px 10px rgba(0, 0, 0, 0.035),
|
||||
0 22.3px 17.9px rgba(0, 0, 0, 0.042),
|
||||
0 41.8px 33.4px rgba(0, 0, 0, 0.05),
|
||||
0 100px 80px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
#menu li a {
|
||||
padding: 10px 0;
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#menu li a:hover {
|
||||
color: #adadad;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
#menuToggle input:checked ~ ul {
|
||||
transform: scale(1, 1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active input:checked ~ span:nth-child(2),
|
||||
.active input:checked ~ span:nth-child(3),
|
||||
.active input:checked ~ span:nth-child(4) {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.active span {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1458px) {
|
||||
#menuToggle span {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
/* Top bar → rotates */
|
||||
#menuToggle input:checked ~ span:nth-child(2) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Middle bar → fades out */
|
||||
#menuToggle input:checked ~ span:nth-child(3) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Bottom bar → rotates opposite */
|
||||
#menuToggle input:checked ~ span:nth-child(4) {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1159px) {
|
||||
header {
|
||||
padding-top: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
header {
|
||||
padding: 110px 20px 10px 20px;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: 100vw;
|
||||
top: -110px;
|
||||
margin: 0px 0 0 0;
|
||||
right: -20px;
|
||||
border-radius: 0;
|
||||
border-radius: 0% 0% 80% 80%;
|
||||
transform: translate(0%, -100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
<script>
|
||||
import { locale, SUPPORTED_LOCALES } from '$lib/translations';
|
||||
|
||||
const isEnglish = $derived($locale === SUPPORTED_LOCALES.EN_US);
|
||||
|
||||
/**
|
||||
* Switches the current locale to the opposite language (EN_US <-> PT_BR).
|
||||
* Prevents the default link behavior (e.g., page jump).
|
||||
*/
|
||||
const onSwitchToOppositeLang = () => {
|
||||
$locale =
|
||||
$locale === SUPPORTED_LOCALES.EN_US
|
||||
? SUPPORTED_LOCALES.PT_BR
|
||||
: SUPPORTED_LOCALES.EN_US;
|
||||
};
|
||||
</script>
|
||||
|
||||
<center>
|
||||
<div class="switch">
|
||||
<input
|
||||
id="language-toggle"
|
||||
class="check-toggle check-toggle-round-flat"
|
||||
type="checkbox"
|
||||
onclick={onSwitchToOppositeLang}
|
||||
checked={isEnglish}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
onSwitchToOppositeLang();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label for="language-toggle"></label>
|
||||
<span class="off">EN</span>
|
||||
<span class="on">PT</span>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.switch > span {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
pointer-events: none;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input.check-toggle-round-flat:checked ~ .off {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
input.check-toggle-round-flat:checked ~ .on {
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.switch > span.on {
|
||||
left: 0;
|
||||
padding-left: 2px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.switch > span.off {
|
||||
right: 0;
|
||||
padding-right: 4px;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.check-toggle {
|
||||
position: absolute;
|
||||
margin-left: -9999px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.check-toggle + label {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input.check-toggle-round-flat + label {
|
||||
padding: 2px;
|
||||
width: 97px;
|
||||
height: 35px;
|
||||
background-color: white;
|
||||
-webkit-border-radius: 60px;
|
||||
-moz-border-radius: 60px;
|
||||
-ms-border-radius: 60px;
|
||||
-o-border-radius: 60px;
|
||||
border-radius: 60px;
|
||||
}
|
||||
input.check-toggle-round-flat + label:before,
|
||||
input.check-toggle-round-flat + label:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
input.check-toggle-round-flat + label:before {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
background-color: white;
|
||||
|
||||
-moz-border-radius: 60px;
|
||||
-ms-border-radius: 60px;
|
||||
-o-border-radius: 60px;
|
||||
border-radius: 60px;
|
||||
}
|
||||
input.check-toggle-round-flat + label:after {
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
width: 48px;
|
||||
background-color: var(--color-primary);
|
||||
-webkit-border-radius: 52px;
|
||||
-moz-border-radius: 52px;
|
||||
-ms-border-radius: 52px;
|
||||
-o-border-radius: 52px;
|
||||
border-radius: 52px;
|
||||
-webkit-transition: margin 0.2s;
|
||||
-moz-transition: margin 0.2s;
|
||||
-o-transition: margin 0.2s;
|
||||
transition: margin 0.2s;
|
||||
}
|
||||
|
||||
input.check-toggle-round-flat:checked + label:after {
|
||||
margin-left: 42px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 -960 960 960"
|
||||
width={size}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
|
||||
</svg>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill={color}
|
||||
aria-hidden="true"
|
||||
id="Bolt--Streamline-Heroicons"
|
||||
height={size}
|
||||
width={size}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.743333333333332 1.0633333333333332a0.5 0.5 0 0 1 0.23933333333333331 0.568L8.654666666666666 6.5h4.845333333333333a0.5 0.5 0 0 1 0.36533333333333334 0.8413333333333333l-7 7.5a0.5 0.5 0 0 1 -0.848 -0.4733333333333333l1.3279999999999998 -4.867999999999999H2.5a0.5 0.5 0 0 1 -0.36533333333333334 -0.8413333333333333l7 -7.5a0.5 0.5 0 0 1 0.6086666666666667 -0.09533333333333333Z"
|
||||
clip-rule="evenodd"
|
||||
stroke-width="0.6667"
|
||||
></path>
|
||||
</svg>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 115.03736 82.932701"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><defs id="defs1" /><g id="layer1" transform="translate(46.342105,-326.35286)"
|
||||
><path
|
||||
style={`fill:${color}`}
|
||||
d="m -33.360673,409.11068 c -0.52052,-0.66147 -13.05506,-29.41883 -12.98111,-29.7819 0.0676,-0.33192 10.25008,-3.75875 11.16875,-3.75875 0.39694,0 -0.23972,-1.36265 6.66385,14.2629 3.11223,7.04422 5.6586,13.00337 5.6586,13.24254 0,0.28356 -1.49602,1.30029 -4.29948,2.92205 -2.36471,1.36794 -4.67009,2.70349 -5.12306,2.96789 -0.65389,0.38166 -0.87798,0.4116 -1.08755,0.14527 z m 51.11115,-7.18941 c -1.0255,-0.19444 -6.26425,-1.60693 -11.6416602,-3.13887 -11.60606,-3.30637 -11.88394,-3.36862 -15.03947,-3.36862 -3.0879598,0 -5.4300198,0.58865 -9.3851798,2.35884 -1.65323,0.73993 -3.20801,1.34533 -3.45507,1.34533 -0.33715,0 -1.59126,-2.59045 -5.02761,-10.38489 -2.51812,-5.7117 -4.63162,-10.51906 -4.69666,-10.68304 -0.065,-0.16398 0.75205,-0.77969 1.81577,-1.36826 1.06372,-0.58856 3.06512,-1.98586 4.44757,-3.1051 1.38245,-1.11925 3.21341,-2.51262 4.06879,-3.09639 5.178,-3.5338 12.3545098,-5.07308 18.9419698,-4.06285 3.59287,0.55099 6.45084,1.42963 10.85591,3.33749 7.3390302,3.17857 11.7920602,4.46814 22.2961402,6.45677 6.83656,1.29429 8.39146,1.84388 9.38771,3.31816 1.351,1.99922 0.46493,4.08861 -2.34074,5.5196 -2.74024,1.39761 -3.54371,1.37687 -13.73269,-0.35444 -8.2986,-1.4101 -9.07435,-1.4986 -12.17084,-1.38851 -2.5834002,0.0919 -3.7126702,0.25703 -5.1593702,0.75463 -1.63768,0.56329 -1.86179,0.72144 -1.93595,1.36615 -0.12182,1.05906 0.46688,1.15204 2.81088,0.44394 3.5053302,-1.05892 6.4809702,-0.89831 16.9844402,0.91671 8.37829,1.44778 8.95895,1.5149 10.60612,1.22601 4.34498,-0.76205 7.62602,-3.6216 7.64432,-6.66234 0.003,-0.56374 -0.31433,-1.57393 -0.76375,-2.42828 -0.88082,-1.67447 -0.97521,-1.4883 1.59586,-3.14763 1.45305,-0.93779 3.67139,-2.74223 8.64098,-7.02876 5.07066,-4.37371 7.8612,-5.88252 9.87621,-5.33994 0.5539,0.14915 1.07867,0.38701 1.16616,0.52858 0.0875,0.14156 0.75876,0.25739 1.49171,0.25739 2.23308,0 3.66323,1.33775 3.66323,3.42656 0,1.22305 -0.39272,1.86203 -5.78067,9.40573 -5.63224,7.88573 -7.41725,9.89443 -10.15569,11.42836 -5.52566,3.09519 -23.79832,12.65577 -25.06445,13.11418 -1.88226,0.68147 -7.21439,0.87102 -9.94397,0.35349 z m 8.93119,-37.4527 c -10.54204,-7.34003 -15.45466,-12.20175 -18.3280302,-18.13812 -2.47555,-5.11448 -2.73882,-10.04227 -0.74529,-13.94992 0.92633,-1.81576 3.3031302,-4.09338 5.2439402,-5.02511 3.14838,-1.51146 7.45008,-1.29668 10.56261,0.52739 1.77607,1.04084 4.13023,3.35279 5.47731,5.37909 0.66799,1.00478 1.26128,1.82688 1.31843,1.82688 0.0571,0 0.69136,-0.8632 1.40937,-1.91822 2.27222,-3.33879 4.61009,-5.23911 7.75182,-6.30101 2.33309,-0.78858 5.77205,-0.63428 7.95915,0.35711 1.75477,0.79542 4.18097,2.97573 5.14853,4.62674 4.08542,6.97124 0.45301,16.98315 -9.18256,25.30961 -4.39191,3.79521 -12.25534,9.42128 -13.13598,9.39843 -0.27047,-0.007 -1.83615,-0.94881 -3.4793,-2.09287 z"
|
||||
id="path2"
|
||||
/></g
|
||||
></svg
|
||||
>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
fill={color}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
id="Flat"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M213.65723,66.34326l-40-40A8.00076,8.00076,0,0,0,168,24H88A16.01833,16.01833,0,0,0,72,40V56H56A16.01833,16.01833,0,0,0,40,72V216a16.01833,16.01833,0,0,0,16,16H168a16.01833,16.01833,0,0,0,16-16V200h16a16.01833,16.01833,0,0,0,16-16V72A8.00035,8.00035,0,0,0,213.65723,66.34326ZM136,192H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm0-32H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm64,24H184V104a8.00035,8.00035,0,0,0-2.34277-5.65674l-40-40A8.00076,8.00076,0,0,0,136,56H88V40h76.68652L200,75.314Z"
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1.5" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1.5" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" />
|
||||
</svg>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let strokeWidth = 2;
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" ry="2" />
|
||||
<polyline points="3,7 12,13 21,7" />
|
||||
</svg>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<script>
|
||||
export let size = 20;
|
||||
export let color = 'currentColor';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
fill={color}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="lock-check"
|
||||
class="icon glyph"
|
||||
><path
|
||||
d="M18,8H17V7A5,5,0,0,0,7,7V8H6a2,2,0,0,0-2,2V20a2,2,0,0,0,2,2H18a2,2,0,0,0,2-2V10A2,2,0,0,0,18,8ZM9,7a3,3,0,0,1,6,0V8H9Zm6.71,6.71-4,4a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42L11,15.59l3.29-3.3a1,1,0,0,1,1.42,1.42Z"
|
||||
></path></svg
|
||||
>
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
import { supportedFormats } from '$lib/format-readers';
|
||||
import { jDataView } from './jdataview';
|
||||
import { Pattern } from './pattern';
|
||||
|
||||
/**
|
||||
* Render the embroidery pattern file to the provided canvas and update views.
|
||||
* @param {string} filename - The name of the file.
|
||||
* @param {ProgressEvent<FileReader>} evt - The file load event.
|
||||
* @param {HTMLCanvasElement} canvas - Canvas to render the pattern.
|
||||
* @param {HTMLElement} colorView - Element to display colors.
|
||||
* @param {HTMLElement} stitchesView - Element to display stitch count.
|
||||
* @param {HTMLElement} sizeView - Element to display size.
|
||||
* @param {{stitches: string, dimensions: string}} localizedStrings - Localized labels.
|
||||
*/
|
||||
function renderFile(
|
||||
filename,
|
||||
evt,
|
||||
canvas,
|
||||
colorView,
|
||||
stitchesView,
|
||||
sizeView,
|
||||
localizedStrings,
|
||||
) {
|
||||
const fileExtension = filename.toLowerCase().split('.').pop();
|
||||
const arrayBuffer = evt.target?.result;
|
||||
|
||||
if (!(fileExtension && arrayBuffer)) {
|
||||
throw new Error('Invalid file extension or file data');
|
||||
}
|
||||
|
||||
const view = new jDataView(arrayBuffer, 0, evt.total || 0);
|
||||
const pattern = new Pattern();
|
||||
|
||||
const formatReader = supportedFormats[fileExtension];
|
||||
if (!formatReader || typeof formatReader.read !== 'function') {
|
||||
throw new Error(`Unsupported file format: ${fileExtension}`);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
formatReader.read(view, pattern);
|
||||
|
||||
pattern.moveToPositive();
|
||||
pattern.drawShapeTo(canvas);
|
||||
pattern.drawColorsTo(colorView);
|
||||
pattern.drawStitchesCountTo(stitchesView, localizedStrings.stitches);
|
||||
pattern.drawSizeValuesTo(sizeView, localizedStrings.dimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a generic abort message.
|
||||
* @param {HTMLElement} errorMessageRef - Element to display the message.
|
||||
*/
|
||||
function renderAbortMessage(errorMessageRef) {
|
||||
errorMessageRef.textContent = 'Render aborted!';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a detailed error message based on error type.
|
||||
* @param {string} errorName - The name of the error.
|
||||
* @param {HTMLElement} errorMessageRef - Element to display the message.
|
||||
*/
|
||||
function renderErrorMessage(errorName, errorMessageRef) {
|
||||
/** @type {string} */
|
||||
let message;
|
||||
|
||||
switch (errorName) {
|
||||
case 'NotFoundError':
|
||||
message =
|
||||
'The file could not be found at the time the read was processed.';
|
||||
break;
|
||||
case 'SecurityError':
|
||||
message =
|
||||
'<p>A file security error occurred. This can be due to:</p>' +
|
||||
'<ul>' +
|
||||
'<li>Accessing certain files deemed unsafe for Web applications.</li>' +
|
||||
'<li>Performing too many read calls on file resources.</li>' +
|
||||
'<li>The file has changed on disk since the user selected it.</li>' +
|
||||
'</ul>';
|
||||
break;
|
||||
case 'NotReadableError':
|
||||
message =
|
||||
'The file cannot be read. This can occur if the file is open in another application.';
|
||||
break;
|
||||
case 'EncodingError':
|
||||
message = 'The length of the data URL for the file is too long.';
|
||||
break;
|
||||
default:
|
||||
message = 'Something went wrong!';
|
||||
break;
|
||||
}
|
||||
|
||||
errorMessageRef.innerHTML = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file and render its pattern to canvas with error handling.
|
||||
* @param {File} fileObject - The file to read.
|
||||
* @param {HTMLCanvasElement} canvas - The canvas to render on.
|
||||
* @param {HTMLElement} errorMessageRef - Element to show error messages.
|
||||
* @param {HTMLElement} colorView - Element to display colors.
|
||||
* @param {HTMLElement} stitchesView - Element to display stitch count.
|
||||
* @param {HTMLElement} sizeView - Element to display size.
|
||||
* @param {{stitches: string, dimensions: string}} localizedStrings - Localized strings.
|
||||
* @returns {string} Empty string after starting file read.
|
||||
*/
|
||||
export default function renderFileToCanvas(
|
||||
fileObject,
|
||||
canvas,
|
||||
errorMessageRef,
|
||||
colorView,
|
||||
stitchesView,
|
||||
sizeView,
|
||||
localizedStrings,
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = (evt) =>
|
||||
renderFile(
|
||||
fileObject.name,
|
||||
evt,
|
||||
canvas,
|
||||
colorView,
|
||||
stitchesView,
|
||||
sizeView,
|
||||
localizedStrings,
|
||||
);
|
||||
|
||||
reader.onabort = () => renderAbortMessage(errorMessageRef);
|
||||
reader.onerror = (evt) =>
|
||||
renderErrorMessage(
|
||||
// @ts-ignore
|
||||
evt.target.error?.name || 'UnknownError',
|
||||
errorMessageRef,
|
||||
);
|
||||
|
||||
if (fileObject) {
|
||||
reader.readAsArrayBuffer(fileObject);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
|
@ -1,331 +0,0 @@
|
|||
import { rgbToHex } from '$lib/utils/rgbToHex';
|
||||
import { shadeColor } from '$lib/utils/shadeColor';
|
||||
|
||||
/**
|
||||
* Represents a single stitch in the pattern.
|
||||
* @param {number} x - The absolute x position of the stitch.
|
||||
* @param {number} y - The absolute y position of the stitch.
|
||||
* @param {number} flags - Stitch flags (e.g. jump, trim).
|
||||
* @param {number} color - Index of the stitch color.
|
||||
* @constructor
|
||||
*/
|
||||
function Stitch(x, y, flags, color) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.flags = flags;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a color with RGB components and an optional description.
|
||||
* @param {number} r - Red component (0-255).
|
||||
* @param {number} g - Green component (0-255).
|
||||
* @param {number} b - Blue component (0-255).
|
||||
* @param {string} description - Color description.
|
||||
* @constructor
|
||||
*/
|
||||
function Color(r, g, b, description) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stitch type bit flags.
|
||||
* @readonly
|
||||
* @enum {number}
|
||||
*/
|
||||
const stitchTypes = {
|
||||
normal: 0,
|
||||
jump: 1,
|
||||
trim: 2,
|
||||
stop: 4,
|
||||
end: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an embroidery pattern containing stitches and colors.
|
||||
* @constructor
|
||||
*/
|
||||
function Pattern() {
|
||||
/** @type {Color[]} */
|
||||
this.colors = [];
|
||||
|
||||
/** @type {Stitch[]} */
|
||||
this.stitches = [];
|
||||
|
||||
/** Hoop info (not typed, depends on implementation) */
|
||||
this.hoop = {};
|
||||
|
||||
/** Last stitch position for relative moves */
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
|
||||
/** Bounding box */
|
||||
this.top = 0;
|
||||
this.bottom = 0;
|
||||
this.left = 0;
|
||||
this.right = 0;
|
||||
|
||||
/** Current color index used for new stitches */
|
||||
this.currentColorIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a color by RGB values.
|
||||
* @param {number} r
|
||||
* @param {number} g
|
||||
* @param {number} b
|
||||
* @param {string} description
|
||||
*/
|
||||
Pattern.prototype.addColorRgb = function (r, g, b, description) {
|
||||
this.colors.push(new Color(r, g, b, description));
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds an existing Color instance.
|
||||
* @param {Color} color
|
||||
*/
|
||||
Pattern.prototype.addColor = function (color) {
|
||||
this.colors.push(color);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a stitch at absolute coordinates.
|
||||
* @param {number} x - Absolute X coordinate.
|
||||
* @param {number} y - Absolute Y coordinate.
|
||||
* @param {number} flags - Stitch flags.
|
||||
* @param {boolean} isAutoColorIndex - Whether to automatically increment color on stop.
|
||||
*/
|
||||
Pattern.prototype.addStitchAbs = function (x, y, flags, isAutoColorIndex) {
|
||||
if ((flags & stitchTypes.end) === stitchTypes.end) {
|
||||
this.calculateBoundingBox();
|
||||
this.fixColorCount();
|
||||
}
|
||||
if (
|
||||
(flags & stitchTypes.stop) === stitchTypes.stop &&
|
||||
this.stitches.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if ((flags & stitchTypes.stop) === stitchTypes.stop && isAutoColorIndex) {
|
||||
this.currentColorIndex += 1;
|
||||
}
|
||||
this.stitches.push(new Stitch(x, y, flags, this.currentColorIndex));
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a stitch relative to the last stitch.
|
||||
* @param {number} dx - Delta X from last stitch.
|
||||
* @param {number} dy - Delta Y from last stitch.
|
||||
* @param {number} flags - Stitch flags.
|
||||
* @param {boolean} [isAutoColorIndex=false] - Whether to automatically increment color on stop. Optional.
|
||||
*/
|
||||
Pattern.prototype.addStitchRel = function (dx, dy, flags, isAutoColorIndex) {
|
||||
if (typeof isAutoColorIndex === 'undefined') {
|
||||
isAutoColorIndex = false;
|
||||
}
|
||||
|
||||
if (this.stitches.length !== 0) {
|
||||
const nx = this.lastX + dx;
|
||||
const ny = this.lastY + dy;
|
||||
this.lastX = nx;
|
||||
this.lastY = ny;
|
||||
this.addStitchAbs(nx, ny, flags, isAutoColorIndex);
|
||||
} else {
|
||||
this.addStitchAbs(dx, dy, flags, isAutoColorIndex);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the bounding box of all stitches, excluding trims.
|
||||
*/
|
||||
Pattern.prototype.calculateBoundingBox = function () {
|
||||
const stitchCount = this.stitches.length;
|
||||
if (stitchCount === 0) {
|
||||
this.bottom = 1;
|
||||
this.right = 1;
|
||||
return;
|
||||
}
|
||||
this.left = Infinity;
|
||||
this.top = Infinity;
|
||||
this.right = -Infinity;
|
||||
this.bottom = -Infinity;
|
||||
|
||||
for (let i = 0; i < stitchCount; i++) {
|
||||
const pt = this.stitches[i];
|
||||
if (!(pt.flags & stitchTypes.trim)) {
|
||||
if (pt.x < this.left) this.left = pt.x;
|
||||
if (pt.y < this.top) this.top = pt.y;
|
||||
if (pt.x > this.right) this.right = pt.x;
|
||||
if (pt.y > this.bottom) this.bottom = pt.y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves all stitches so the pattern is positioned at positive coordinates.
|
||||
*/
|
||||
Pattern.prototype.moveToPositive = function () {
|
||||
const stitchCount = this.stitches.length;
|
||||
for (let i = 0; i < stitchCount; i++) {
|
||||
this.stitches[i].x -= this.left;
|
||||
this.stitches[i].y -= this.top;
|
||||
}
|
||||
this.right -= this.left;
|
||||
this.left = 0;
|
||||
this.bottom -= this.top;
|
||||
this.top = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Flips the pattern vertically.
|
||||
*/
|
||||
Pattern.prototype.invertPatternVertical = function () {
|
||||
const stitchCount = this.stitches.length;
|
||||
const tempTop = -this.top;
|
||||
for (let i = 0; i < stitchCount; i++) {
|
||||
this.stitches[i].y = -this.stitches[i].y;
|
||||
}
|
||||
this.top = -this.bottom;
|
||||
this.bottom = tempTop;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a random color to the pattern.
|
||||
*/
|
||||
Pattern.prototype.addColorRandom = function () {
|
||||
this.colors.push(
|
||||
new Color(
|
||||
Math.round(Math.random() * 256),
|
||||
Math.round(Math.random() * 256),
|
||||
Math.round(Math.random() * 256),
|
||||
'random',
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fixes the color count so colors match used indices in stitches.
|
||||
*/
|
||||
Pattern.prototype.fixColorCount = function () {
|
||||
let maxColorIndex = 0;
|
||||
const stitchCount = this.stitches.length;
|
||||
|
||||
for (let i = 0; i < stitchCount; i++) {
|
||||
if (this.stitches[i].color > maxColorIndex) {
|
||||
maxColorIndex = this.stitches[i].color;
|
||||
}
|
||||
}
|
||||
|
||||
while (this.colors.length <= maxColorIndex) {
|
||||
this.addColorRandom();
|
||||
}
|
||||
|
||||
// Remove extra colors beyond max used index
|
||||
this.colors.splice(maxColorIndex + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws the stitch pattern to a canvas element.
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
*/
|
||||
Pattern.prototype.drawShapeTo = function (canvas) {
|
||||
canvas.width = this.right;
|
||||
canvas.height = this.bottom;
|
||||
|
||||
let gradient, tx, ty;
|
||||
let lastStitch = this.stitches[0];
|
||||
let gWidth = 100;
|
||||
|
||||
if (canvas.getContext) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
// If context is null, just return or handle accordingly
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
let color = this.colors[this.stitches[0].color];
|
||||
for (let i = 0; i < this.stitches.length; i++) {
|
||||
const currentStitch = this.stitches[i];
|
||||
if (i > 0) lastStitch = this.stitches[i - 1];
|
||||
tx = currentStitch.x - lastStitch.x;
|
||||
ty = currentStitch.y - lastStitch.y;
|
||||
|
||||
gWidth = Math.sqrt(tx * tx + ty * ty);
|
||||
gradient = ctx.createRadialGradient(
|
||||
currentStitch.x - tx,
|
||||
currentStitch.y - ty,
|
||||
0,
|
||||
currentStitch.x - tx,
|
||||
currentStitch.y - ty,
|
||||
gWidth * 1.4,
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, shadeColor(rgbToHex(color), -60));
|
||||
gradient.addColorStop(0.05, rgbToHex(color));
|
||||
gradient.addColorStop(0.5, shadeColor(rgbToHex(color), 60));
|
||||
gradient.addColorStop(0.9, rgbToHex(color));
|
||||
gradient.addColorStop(1.0, shadeColor(rgbToHex(color), -60));
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
if (
|
||||
currentStitch.flags === stitchTypes.jump ||
|
||||
currentStitch.flags === stitchTypes.trim ||
|
||||
currentStitch.flags === stitchTypes.stop
|
||||
) {
|
||||
color = this.colors[currentStitch.color];
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle =
|
||||
'rgba(' + color.r + ',' + color.g + ',' + color.b + ',0)';
|
||||
ctx.moveTo(currentStitch.x, currentStitch.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastStitch.x, lastStitch.y);
|
||||
ctx.lineTo(currentStitch.x, currentStitch.y);
|
||||
ctx.stroke();
|
||||
lastStitch = currentStitch;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws color swatches into a container element.
|
||||
* @param {HTMLElement} colorContainer
|
||||
*/
|
||||
Pattern.prototype.drawColorsTo = function (colorContainer) {
|
||||
this.colors.forEach((color) => {
|
||||
colorContainer.innerHTML += `<div style='background-color: rgb(${color.r}, ${color.g}, ${color.b}); height: 25px; width: 25px; border: 1px solid #000000; border-radius: 16px;'></div>`;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the stitch count in a container element.
|
||||
* @param {HTMLElement} stitchesContainer
|
||||
* @param {string} stitchesString - Label for stitches count.
|
||||
*/
|
||||
Pattern.prototype.drawStitchesCountTo = function (
|
||||
stitchesContainer,
|
||||
stitchesString,
|
||||
) {
|
||||
stitchesContainer.innerHTML += `<div><strong>${stitchesString}:</strong> ${this.stitches.length}</div>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays pattern dimensions in a container element.
|
||||
* @param {HTMLElement} sizeContainer
|
||||
* @param {string} dimensionsString - Label for dimensions.
|
||||
*/
|
||||
Pattern.prototype.drawSizeValuesTo = function (
|
||||
sizeContainer,
|
||||
dimensionsString,
|
||||
) {
|
||||
sizeContainer.innerHTML += `<div><strong>${dimensionsString}:</strong> ${Math.round(this.right / 10)}mm x ${Math.round(this.bottom / 10)}mm</div>`;
|
||||
};
|
||||
|
||||
export { Pattern, Color, stitchTypes };
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { stitchTypes } from '$lib/file-renderer/pattern';
|
||||
|
||||
/**
|
||||
* Decodes stitch flags from the 3rd byte of a DST stitch command.
|
||||
*
|
||||
* @param {number} b2 The third byte of the stitch command.
|
||||
* @returns {number} Bitmask representing stitch types.
|
||||
*/
|
||||
function decodeExp(b2) {
|
||||
if (b2 === 0xf3) {
|
||||
return stitchTypes.end;
|
||||
}
|
||||
if ((b2 & 0xc3) === 0xc3) {
|
||||
return stitchTypes.trim | stitchTypes.stop;
|
||||
}
|
||||
|
||||
let returnCode = 0;
|
||||
if (b2 & 0x80) {
|
||||
returnCode |= stitchTypes.trim;
|
||||
}
|
||||
if (b2 & 0x40) {
|
||||
returnCode |= stitchTypes.stop;
|
||||
}
|
||||
return returnCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a DST embroidery file and populates the pattern object.
|
||||
*
|
||||
*
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern
|
||||
*/
|
||||
export function dstRead(file, pattern) {
|
||||
let prevJump = false;
|
||||
const byteCount = file.byteLength;
|
||||
|
||||
file.seek(512); // Skip DST header
|
||||
|
||||
while (file.tell() < byteCount - 3) {
|
||||
/** @type {number[]} */
|
||||
const b = [file.getUint8(), file.getUint8(), file.getUint8()];
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
// Decode X movements
|
||||
if (b[0] & 0x01) x += 1;
|
||||
if (b[0] & 0x02) x -= 1;
|
||||
if (b[0] & 0x04) x += 9;
|
||||
if (b[0] & 0x08) x -= 9;
|
||||
|
||||
if (b[1] & 0x01) x += 3;
|
||||
if (b[1] & 0x02) x -= 3;
|
||||
if (b[1] & 0x04) x += 27;
|
||||
if (b[1] & 0x08) x -= 27;
|
||||
|
||||
if (b[2] & 0x04) x += 81;
|
||||
if (b[2] & 0x08) x -= 81;
|
||||
|
||||
// Decode Y movements
|
||||
if (b[0] & 0x80) y += 1;
|
||||
if (b[0] & 0x40) y -= 1;
|
||||
if (b[0] & 0x20) y += 9;
|
||||
if (b[0] & 0x10) y -= 9;
|
||||
|
||||
if (b[1] & 0x80) y += 3;
|
||||
if (b[1] & 0x40) y -= 3;
|
||||
if (b[1] & 0x20) y += 27;
|
||||
if (b[1] & 0x10) y -= 27;
|
||||
|
||||
if (b[2] & 0x20) y += 81;
|
||||
if (b[2] & 0x10) y -= 81;
|
||||
|
||||
let flags = decodeExp(b[2]);
|
||||
const thisJump = (flags & stitchTypes.jump) !== 0;
|
||||
|
||||
if (prevJump) {
|
||||
flags |= stitchTypes.jump;
|
||||
}
|
||||
|
||||
pattern.addStitchRel(x, y, flags, true);
|
||||
|
||||
prevJump = thisJump;
|
||||
}
|
||||
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { stitchTypes } from '$lib/file-renderer/pattern';
|
||||
|
||||
/**
|
||||
* Decodes a single byte with EXP format rules.
|
||||
* Values above 128 are negative numbers encoded with bitwise operations.
|
||||
*
|
||||
* @param {number} input - A signed 8-bit integer (-128 to 127).
|
||||
* @returns {number} - Decoded signed integer.
|
||||
*/
|
||||
function expDecode(input) {
|
||||
return input > 128 ? -(~input & 0xff) - 1 : input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an EXP format file and adds stitches to the given pattern.
|
||||
*
|
||||
* @param {EmbroideryFileView} file - A DataView representing the binary EXP file content.
|
||||
* @param {EmbroideryPattern} pattern - The pattern object with addStitchRel and invertPatternVertical methods.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function expRead(file, pattern) {
|
||||
let index = 0;
|
||||
const byteCount = file.byteLength;
|
||||
|
||||
while (index < byteCount) {
|
||||
let flags = stitchTypes.normal;
|
||||
let b0 = file.getInt8(index++);
|
||||
let b1 = file.getInt8(index++);
|
||||
|
||||
if (b0 === -128) {
|
||||
if (b1 & 1) {
|
||||
b0 = file.getInt8(index++);
|
||||
b1 = file.getInt8(index++);
|
||||
flags = stitchTypes.stop;
|
||||
} else if (b1 === 2 || b1 === 4) {
|
||||
b0 = file.getInt8(index++);
|
||||
b1 = file.getInt8(index++);
|
||||
flags = stitchTypes.trim;
|
||||
} else if (b1 === -128) {
|
||||
b0 = file.getInt8(index++);
|
||||
b1 = file.getInt8(index++);
|
||||
b0 = 0;
|
||||
b1 = 0;
|
||||
flags = stitchTypes.trim;
|
||||
}
|
||||
}
|
||||
|
||||
const dx = expDecode(b0);
|
||||
const dy = expDecode(b1);
|
||||
|
||||
pattern.addStitchRel(dx, dy, flags, true);
|
||||
}
|
||||
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end);
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { dstRead } from './dst';
|
||||
import { expRead } from './exp';
|
||||
import { jefRead } from './jef';
|
||||
import { pecRead } from './pec';
|
||||
import { pesRead } from './pes';
|
||||
|
||||
/**
|
||||
* Supported embroidery file formats.
|
||||
* @type {SupportedFormats}
|
||||
*/
|
||||
const supportedFormats = {
|
||||
pes: { ext: '.pes', read: pesRead },
|
||||
dst: { ext: '.dst', read: dstRead },
|
||||
pec: { ext: '.pec', read: pecRead },
|
||||
jef: { ext: '.jef', read: jefRead },
|
||||
exp: { ext: '.exp', read: expRead },
|
||||
};
|
||||
|
||||
export { supportedFormats };
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
import { Color, stitchTypes } from '$lib/file-renderer/pattern';
|
||||
|
||||
/** @type {Color[]} */
|
||||
const colors = [
|
||||
new Color(0, 0, 0, 'Black'),
|
||||
new Color(0, 0, 0, 'Black'),
|
||||
new Color(255, 255, 255, 'White'),
|
||||
new Color(255, 255, 23, 'Yellow'),
|
||||
new Color(250, 160, 96, 'Orange'),
|
||||
new Color(92, 118, 73, 'Olive Green'),
|
||||
new Color(64, 192, 48, 'Green'),
|
||||
new Color(101, 194, 200, 'Sky'),
|
||||
new Color(172, 128, 190, 'Purple'),
|
||||
new Color(245, 188, 203, 'Pink'),
|
||||
new Color(255, 0, 0, 'Red'),
|
||||
new Color(192, 128, 0, 'Brown'),
|
||||
new Color(0, 0, 240, 'Blue'),
|
||||
new Color(228, 195, 93, 'Gold'),
|
||||
new Color(165, 42, 42, 'Dark Brown'),
|
||||
new Color(213, 176, 212, 'Pale Violet'),
|
||||
new Color(252, 242, 148, 'Pale Yellow'),
|
||||
new Color(240, 208, 192, 'Pale Pink'),
|
||||
new Color(255, 192, 0, 'Peach'),
|
||||
new Color(201, 164, 128, 'Beige'),
|
||||
new Color(155, 61, 75, 'Wine Red'),
|
||||
new Color(160, 184, 204, 'Pale Sky'),
|
||||
new Color(127, 194, 28, 'Yellow Green'),
|
||||
new Color(185, 185, 185, 'Silver Grey'),
|
||||
new Color(160, 160, 160, 'Grey'),
|
||||
new Color(152, 214, 189, 'Pale Aqua'),
|
||||
new Color(184, 240, 240, 'Baby Blue'),
|
||||
new Color(54, 139, 160, 'Powder Blue'),
|
||||
new Color(79, 131, 171, 'Bright Blue'),
|
||||
new Color(56, 106, 145, 'Slate Blue'),
|
||||
new Color(0, 32, 107, 'Nave Blue'),
|
||||
new Color(229, 197, 202, 'Salmon Pink'),
|
||||
new Color(249, 103, 107, 'Coral'),
|
||||
new Color(227, 49, 31, 'Burnt Orange'),
|
||||
new Color(226, 161, 136, 'Cinnamon'),
|
||||
new Color(181, 148, 116, 'Umber'),
|
||||
new Color(228, 207, 153, 'Blonde'),
|
||||
new Color(225, 203, 0, 'Sunflower'),
|
||||
new Color(225, 173, 212, 'Orchid Pink'),
|
||||
new Color(195, 0, 126, 'Peony Purple'),
|
||||
new Color(128, 0, 75, 'Burgundy'),
|
||||
new Color(160, 96, 176, 'Royal Purple'),
|
||||
new Color(192, 64, 32, 'Cardinal Red'),
|
||||
new Color(202, 224, 192, 'Opal Green'),
|
||||
new Color(137, 152, 86, 'Moss Green'),
|
||||
new Color(0, 170, 0, 'Meadow Green'),
|
||||
new Color(33, 138, 33, 'Dark Green'),
|
||||
new Color(93, 174, 148, 'Aquamarine'),
|
||||
new Color(76, 191, 143, 'Emerald Green'),
|
||||
new Color(0, 119, 114, 'Peacock Green'),
|
||||
new Color(112, 112, 112, 'Dark Grey'),
|
||||
new Color(242, 255, 255, 'Ivory White'),
|
||||
new Color(177, 88, 24, 'Hazel'),
|
||||
new Color(203, 138, 7, 'Toast'),
|
||||
new Color(247, 146, 123, 'Salmon'),
|
||||
new Color(152, 105, 45, 'Cocoa Brown'),
|
||||
new Color(162, 113, 72, 'Sienna'),
|
||||
new Color(123, 85, 74, 'Sepia'),
|
||||
new Color(79, 57, 70, 'Dark Sepia'),
|
||||
new Color(82, 58, 151, 'Violet Blue'),
|
||||
new Color(0, 0, 160, 'Blue Ink'),
|
||||
new Color(0, 150, 222, 'Solar Blue'),
|
||||
new Color(178, 221, 83, 'Green Dust'),
|
||||
new Color(250, 143, 187, 'Crimson'),
|
||||
new Color(222, 100, 158, 'Floral Pink'),
|
||||
new Color(181, 80, 102, 'Wine'),
|
||||
new Color(94, 87, 71, 'Olive Drab'),
|
||||
new Color(76, 136, 31, 'Meadow'),
|
||||
new Color(228, 220, 121, 'Mustard'),
|
||||
new Color(203, 138, 26, 'Yellow Ochre'),
|
||||
new Color(198, 170, 66, 'Old Gold'),
|
||||
new Color(236, 176, 44, 'Honeydew'),
|
||||
new Color(248, 128, 64, 'Tangerine'),
|
||||
new Color(255, 229, 5, 'Canary Yellow'),
|
||||
new Color(250, 122, 122, 'Vermillion'),
|
||||
new Color(107, 224, 0, 'Bright Green'),
|
||||
new Color(56, 108, 174, 'Ocean Blue'),
|
||||
new Color(227, 196, 180, 'Beige Grey'),
|
||||
new Color(227, 172, 129, 'Bamboo'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Decode a single byte for JEF stitch data (signed int8-like with special encoding).
|
||||
* @param {number} byte
|
||||
* @returns {number}
|
||||
*/
|
||||
const jefDecode = (byte) => (byte >= 0x80 ? -(~byte & 0xff) - 1 : byte);
|
||||
|
||||
/**
|
||||
* Check if a byte represents a special stitch (0x80).
|
||||
* @param {number} byte
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isSpecialStitch = (byte) => byte === 0x80;
|
||||
|
||||
/**
|
||||
* Check if a byte represents a stop or trim command.
|
||||
* @param {number} byte
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isStopOrTrim = (byte) =>
|
||||
(byte & 0x01) !== 0 || byte === 0x02 || byte === 0x04;
|
||||
|
||||
/**
|
||||
* Check if a byte indicates end of pattern.
|
||||
* @param {number} byte
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isEndOfPattern = (byte) => byte === 0x10;
|
||||
|
||||
/**
|
||||
* Check if a byte indicates a stop command.
|
||||
* @param {number} byte
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isStop = (byte) => (byte & 0x01) !== 0;
|
||||
|
||||
/**
|
||||
* Read two stitch data bytes from the file.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @returns {{ byte1: number, byte2: number }}
|
||||
*/
|
||||
const readStitchData = (file) => ({
|
||||
byte1: file.getUint8(),
|
||||
byte2: file.getUint8(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Add colors from file data to the pattern.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern
|
||||
* @param {number} colorCount
|
||||
*/
|
||||
const addColorsToPattern = (file, pattern, colorCount) => {
|
||||
for (let i = 0; i < colorCount; i++) {
|
||||
const colorIndex = file.getUint32(file.tell(), true) % colors.length;
|
||||
pattern.addColor(colors[colorIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the stitch type and potentially read additional bytes.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {number} byte1
|
||||
* @param {number} byte2
|
||||
* @returns {{ type: number, byte1: number, byte2: number, end?: boolean }}
|
||||
*/
|
||||
const determineStitchType = (file, byte1, byte2) => {
|
||||
if (isSpecialStitch(byte1)) {
|
||||
if (isStopOrTrim(byte2)) {
|
||||
return {
|
||||
type: isStop(byte2) ? stitchTypes.stop : stitchTypes.trim,
|
||||
byte1: file.getUint8(),
|
||||
byte2: file.getUint8(),
|
||||
};
|
||||
} else if (isEndOfPattern(byte2)) {
|
||||
return { type: stitchTypes.end, byte1: 0, byte2: 0, end: true };
|
||||
}
|
||||
}
|
||||
return { type: stitchTypes.normal, byte1, byte2 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Process stitches in the file and add them to the pattern.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern
|
||||
* @param {number} stitchCount
|
||||
*/
|
||||
const processStitches = (file, pattern, stitchCount) => {
|
||||
let stitchesProcessed = 0;
|
||||
while (stitchesProcessed < stitchCount + 100) {
|
||||
const { byte1, byte2 } = readStitchData(file);
|
||||
const {
|
||||
type,
|
||||
byte1: decodedByte1,
|
||||
byte2: decodedByte2,
|
||||
end,
|
||||
} = determineStitchType(file, byte1, byte2);
|
||||
pattern.addStitchRel(
|
||||
jefDecode(decodedByte1),
|
||||
jefDecode(decodedByte2),
|
||||
type,
|
||||
true,
|
||||
);
|
||||
if (end) break;
|
||||
stitchesProcessed++;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a JEF file and adds stitches and colors to the pattern.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern
|
||||
*/
|
||||
export function jefRead(file, pattern) {
|
||||
file.seek(24);
|
||||
const colorCount = file.getInt32(file.tell(), true);
|
||||
const stitchCount = file.getInt32(file.tell(), true);
|
||||
file.seek(file.tell() + 84);
|
||||
|
||||
addColorsToPattern(file, pattern, colorCount);
|
||||
file.seek(file.tell() + (6 - colorCount) * 4);
|
||||
|
||||
processStitches(file, pattern, stitchCount);
|
||||
pattern.invertPatternVertical();
|
||||
}
|
||||
|
||||
export const jefColors = colors;
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
import { Color, stitchTypes } from '$lib/file-renderer/pattern';
|
||||
|
||||
/**
|
||||
* Array of predefined embroidery colors used in PEC files.
|
||||
* @type {Color[]}
|
||||
*/
|
||||
export const pecColors = [
|
||||
new Color(0, 0, 0, 'Unknown'),
|
||||
new Color(14, 31, 124, 'Prussian Blue'),
|
||||
new Color(10, 85, 163, 'Blue'),
|
||||
new Color(0, 135, 119, 'Teal Green'),
|
||||
new Color(75, 107, 175, 'Cornflower Blue'),
|
||||
new Color(237, 23, 31, 'Red'),
|
||||
new Color(209, 92, 0, 'Reddish Brown'),
|
||||
new Color(145, 54, 151, 'Magenta'),
|
||||
new Color(228, 154, 203, 'Light Lilac'),
|
||||
new Color(145, 95, 172, 'Lilac'),
|
||||
new Color(158, 214, 125, 'Mint Green'),
|
||||
new Color(232, 169, 0, 'Deep Gold'),
|
||||
new Color(254, 186, 53, 'Orange'),
|
||||
new Color(255, 255, 0, 'Yellow'),
|
||||
new Color(112, 188, 31, 'Lime Green'),
|
||||
new Color(186, 152, 0, 'Brass'),
|
||||
new Color(168, 168, 168, 'Silver'),
|
||||
new Color(125, 111, 0, 'Russet Brown'),
|
||||
new Color(255, 255, 179, 'Cream Brown'),
|
||||
new Color(79, 85, 86, 'Pewter'),
|
||||
new Color(0, 0, 0, 'Black'),
|
||||
new Color(11, 61, 145, 'Ultramarine'),
|
||||
new Color(119, 1, 118, 'Royal Purple'),
|
||||
new Color(41, 49, 51, 'Dark Gray'),
|
||||
new Color(42, 19, 1, 'Dark Brown'),
|
||||
new Color(246, 74, 138, 'Deep Rose'),
|
||||
new Color(178, 118, 36, 'Light Brown'),
|
||||
new Color(252, 187, 197, 'Salmon Pink'),
|
||||
new Color(254, 55, 15, 'Vermillion'),
|
||||
new Color(240, 240, 240, 'White'),
|
||||
new Color(106, 28, 138, 'Violet'),
|
||||
new Color(168, 221, 196, 'Seacrest'),
|
||||
new Color(37, 132, 187, 'Sky Blue'),
|
||||
new Color(254, 179, 67, 'Pumpkin'),
|
||||
new Color(255, 243, 107, 'Cream Yellow'),
|
||||
new Color(208, 166, 96, 'Khaki'),
|
||||
new Color(209, 84, 0, 'Clay Brown'),
|
||||
new Color(102, 186, 73, 'Leaf Green'),
|
||||
new Color(19, 74, 70, 'Peacock Blue'),
|
||||
new Color(135, 135, 135, 'Gray'),
|
||||
new Color(216, 204, 198, 'Warm Gray'),
|
||||
new Color(67, 86, 7, 'Dark Olive'),
|
||||
new Color(253, 217, 222, 'Flesh Pink'),
|
||||
new Color(249, 147, 188, 'Pink'),
|
||||
new Color(0, 56, 34, 'Deep Green'),
|
||||
new Color(178, 175, 212, 'Lavender'),
|
||||
new Color(104, 106, 176, 'Wisteria Violet'),
|
||||
new Color(239, 227, 185, 'Beige'),
|
||||
new Color(247, 56, 102, 'Carmine'),
|
||||
new Color(181, 75, 100, 'Amber Red'),
|
||||
new Color(19, 43, 26, 'Olive Green'),
|
||||
new Color(199, 1, 86, 'Dark Fuschia'),
|
||||
new Color(254, 158, 50, 'Tangerine'),
|
||||
new Color(168, 222, 235, 'Light Blue'),
|
||||
new Color(0, 103, 62, 'Emerald Green'),
|
||||
new Color(78, 41, 144, 'Purple'),
|
||||
new Color(47, 126, 32, 'Moss Green'),
|
||||
new Color(255, 204, 204, 'Flesh Pink'),
|
||||
new Color(255, 217, 17, 'Harvest Gold'),
|
||||
new Color(9, 91, 166, 'Electric Blue'),
|
||||
new Color(240, 249, 112, 'Lemon Yellow'),
|
||||
new Color(227, 243, 91, 'Fresh Green'),
|
||||
new Color(255, 153, 0, 'Orange'),
|
||||
new Color(255, 240, 141, 'Cream Yellow'),
|
||||
new Color(255, 200, 200, 'Applique'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Reads stitch data from a PEC embroidery file and adds it to the pattern.
|
||||
* @param {EmbroideryFileView} file
|
||||
* @param {EmbroideryPattern} pattern - The pattern to populate.
|
||||
*/
|
||||
function readPecStitches(file, pattern) {
|
||||
let stitchNumber = 0;
|
||||
const byteCount = file.byteLength;
|
||||
|
||||
while (file.tell() < byteCount) {
|
||||
let [xOffset, yOffset] = [file.getUint8(), file.getUint8()];
|
||||
let stitchType = stitchTypes.normal;
|
||||
|
||||
if (isEndStitch(xOffset, yOffset)) {
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end, true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isStopStitch(xOffset, yOffset)) {
|
||||
file.getInt8(); // Skip extra byte
|
||||
pattern.addStitchRel(0, 0, stitchTypes.stop, true);
|
||||
stitchNumber++;
|
||||
continue;
|
||||
}
|
||||
|
||||
stitchType = determineStitchType(xOffset, yOffset);
|
||||
[xOffset, yOffset] = decodeCoordinates(xOffset, yOffset, file);
|
||||
|
||||
pattern.addStitchRel(xOffset, yOffset, stitchType, true);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
stitchNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the stitch is an "end" stitch.
|
||||
* @param {number} xOffset
|
||||
* @param {number} yOffset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEndStitch(xOffset, yOffset) {
|
||||
return xOffset === 0xff && yOffset === 0x00;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the stitch is a "stop" stitch.
|
||||
* @param {number} xOffset
|
||||
* @param {number} yOffset
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isStopStitch(xOffset, yOffset) {
|
||||
return xOffset === 0xfe && yOffset === 0xb0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers the stitch type from byte flags.
|
||||
* @param {number} xOffset
|
||||
* @param {number} yOffset
|
||||
* @returns {number}
|
||||
*/
|
||||
function determineStitchType(xOffset, yOffset) {
|
||||
if (xOffset & 0x80) {
|
||||
if (xOffset & 0x20) return stitchTypes.trim;
|
||||
if (xOffset & 0x10) return stitchTypes.jump;
|
||||
}
|
||||
if (yOffset & 0x80) {
|
||||
if (yOffset & 0x20) return stitchTypes.trim;
|
||||
if (yOffset & 0x10) return stitchTypes.jump;
|
||||
}
|
||||
return stitchTypes.normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes 12-bit signed coordinates from PEC format.
|
||||
* @param {number} xOffset
|
||||
* @param {number} yOffset
|
||||
* @param {DataView & { tell: () => number, seek: (pos: number) => void, getUint8: () => number, getInt8: () => number }} file
|
||||
* @returns {[number, number]} - Decoded [x, y] coordinates.
|
||||
*/
|
||||
function decodeCoordinates(xOffset, yOffset, file) {
|
||||
if (xOffset & 0x80) {
|
||||
xOffset = ((xOffset & 0x0f) << 8) + yOffset;
|
||||
if (xOffset & 0x800) xOffset -= 0x1000;
|
||||
yOffset = file.getUint8();
|
||||
} else if (xOffset >= 0x40) {
|
||||
xOffset -= 0x80;
|
||||
}
|
||||
|
||||
if (yOffset & 0x80) {
|
||||
yOffset = ((yOffset & 0x0f) << 8) + file.getUint8();
|
||||
if (yOffset & 0x800) yOffset -= 0x1000;
|
||||
} else if (yOffset > 0x3f) {
|
||||
yOffset -= 0x80;
|
||||
}
|
||||
|
||||
return [xOffset, yOffset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a PES file and adds stitch and color data to the pattern.
|
||||
* @param {DataView & { tell: () => number, seek: (pos: number) => void, getUint8: () => number, getInt8: () => number }} file
|
||||
* @param {EmbroideryPattern} pattern - The pattern to populate.
|
||||
*/
|
||||
export function pesRead(file, pattern) {
|
||||
const pecStart = file.getInt32(8, true);
|
||||
file.seek(pecStart + 48);
|
||||
|
||||
const numColors = file.getInt8() + 1;
|
||||
for (let i = 0; i < numColors; i++) {
|
||||
pattern.addColor(pecColors[file.getInt8()]);
|
||||
}
|
||||
|
||||
file.seek(pecStart + 532);
|
||||
readPecStitches(file, pattern);
|
||||
pattern.addStitchRel(0, 0, stitchTypes.end);
|
||||
}
|
||||
|
||||
export const pecReadStitches = readPecStitches;
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/**
|
||||
* A custom DataView with embroidery reader-specific helper methods.
|
||||
* @typedef {DataView & {
|
||||
* tell: () => number;
|
||||
* seek: (pos: number) => void;
|
||||
* getUint8: () => number;
|
||||
* getInt8: () => number;
|
||||
* getInt32: (pos: number, littleEndian: boolean) => number;
|
||||
* }} EmbroideryFileView
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Pattern extended with optional embroidery reader methods.
|
||||
* @typedef {import('$lib/file-renderer/pattern').Pattern & {
|
||||
* addColor?: (color: import('$lib/file-renderer/pattern').Color) => void;
|
||||
* addStitchRel: (dx: number, dy: number, stitchType: string, autoAdvance?: boolean) => void;
|
||||
* invertPatternVertical?: () => void;
|
||||
* }} EmbroideryPattern
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a reader for a specific embroidery file format.
|
||||
* @typedef {Object} FormatReader
|
||||
* @property {string} ext - File extension (e.g., '.pes', '.dst').
|
||||
* @property {(view: EmbroideryFileView, pattern: EmbroideryPattern) => void} read - Function to parse the embroidery format and populate the pattern.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A map of supported embroidery file formats keyed by format name (e.g., "pes", "dst").
|
||||
* @typedef {Object.<string, FormatReader>} SupportedFormats
|
||||
*/
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
<script>
|
||||
import { t } from '$lib/translations';
|
||||
</script>
|
||||
|
||||
<section id="faq">
|
||||
<h1>{$t('faq.title')}</h1>
|
||||
<p class="intro">
|
||||
{$t('faq.intro')}
|
||||
</p>
|
||||
|
||||
<div class="faq-list">
|
||||
<details>
|
||||
<summary>{$t('faq.items.openPesOnline.summary')}</summary>
|
||||
<p>{$t('faq.items.openPesOnline.description')}</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>{$t('faq.items.supportedFormats.summary')}</summary>
|
||||
<p>{$t('faq.items.supportedFormats.description')}</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>{$t('faq.items.needSoftware.summary')}</summary>
|
||||
<p>{$t('faq.items.needSoftware.description')}</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>{$t('faq.items.isSafe.summary')}</summary>
|
||||
<p>{$t('faq.items.isSafe.description')}</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>{$t('faq.items.multipleFiles.summary')}</summary>
|
||||
<p>{$t('faq.items.multipleFiles.description')}</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>{$t('faq.items.mobileSupport.summary')}</summary>
|
||||
<p>{$t('faq.items.mobileSupport.description')}</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#faq {
|
||||
padding: 100px 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#faq h1 {
|
||||
font-weight: 700;
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#faq .intro {
|
||||
text-align: center;
|
||||
margin: 10px 0 40px;
|
||||
}
|
||||
|
||||
.faq-list details {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.faq-list summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.faq-list summary::after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
font-size: 1.7rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.faq-list details[open] summary::after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.faq-list p {
|
||||
margin-top: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
import BoltIcon from '$lib/components/icons/BoltIcon.svelte';
|
||||
import PadlockIcon from '$lib/components/icons/PadlockIcon.svelte';
|
||||
import FourSquaresIcon from '$lib/components/icons/FourSquaresIcon.svelte';
|
||||
import FilesIcon from '$lib/components/icons/FilesIcon.svelte';
|
||||
</script>
|
||||
|
||||
<section id="features">
|
||||
<header>
|
||||
<h1>{$t('features.title')}</h1>
|
||||
<p class="subtitle">{$t('features.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<div class="cards-container">
|
||||
<div class="blob">
|
||||
<p class="adjective">{$t('features.cards.fast.adjective')}</p>
|
||||
<h2>{$t('features.cards.fast.title')}</h2>
|
||||
<BoltIcon color="#06345f" size={100} />
|
||||
<p class="description">{$t('features.cards.fast.description')}</p>
|
||||
</div>
|
||||
|
||||
<div class="blob">
|
||||
<p class="adjective">{$t('features.cards.private.adjective')}</p>
|
||||
<h2>{$t('features.cards.private.title')}</h2>
|
||||
<PadlockIcon color="#06345f" size={100} />
|
||||
<p class="description">{$t('features.cards.private.description')}</p>
|
||||
</div>
|
||||
|
||||
<div class="blob">
|
||||
<p class="adjective">{$t('features.cards.optimized.adjective')}</p>
|
||||
<h2>{$t('features.cards.optimized.title')}</h2>
|
||||
<FourSquaresIcon color="#06345f" size={100} />
|
||||
<p class="description">{$t('features.cards.optimized.description')}</p>
|
||||
</div>
|
||||
|
||||
<div class="blob">
|
||||
<p class="adjective">{$t('features.cards.compatibility.adjective')}</p>
|
||||
<h2>{$t('features.cards.compatibility.title')}</h2>
|
||||
<FilesIcon color="#06345f" size={100} />
|
||||
<p class="description">
|
||||
{$t('features.cards.compatibility.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a class="organic-btn-secondary" href={resolve('/viewer')}
|
||||
>{$t('features.cta')}</a
|
||||
>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 100px 0;
|
||||
background-color: var(--color-primary);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#features::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 150 C 60 20, 140 180, 190 50' stroke='white' stroke-width='1.5' stroke-dasharray='4 6' opacity='0.2'/%3E%3Ccircle cx='10' cy='150' r='3' fill='white' opacity='0.25'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 600px;
|
||||
background-position: right -100px top -80px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#features::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 40 C 80 180, 120 0, 180 140' stroke='white' stroke-width='1.2' stroke-dasharray='3 8' opacity='0.15'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 500px;
|
||||
background-position: left -120px bottom -80px;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
header {
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
font-size: 2.7rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 100px 40px;
|
||||
}
|
||||
|
||||
.blob {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
height: 400px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 25% 50% 75% 100%;
|
||||
text-align: center;
|
||||
padding: 30px 60px;
|
||||
|
||||
background: linear-gradient(
|
||||
30deg in oklch shorter hue,
|
||||
oklch(0.98 0.01 260) 10%,
|
||||
oklch(0.92 0.02 260)
|
||||
);
|
||||
|
||||
box-shadow: 1rem 1rem 50px #0001;
|
||||
}
|
||||
|
||||
.blob h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1rem;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.adjective {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.8rem !important;
|
||||
}
|
||||
|
||||
.organic-btn-secondary {
|
||||
font-size: 1.3rem;
|
||||
padding: 30px 90px;
|
||||
}
|
||||
|
||||
@media (max-width: 1639px) {
|
||||
.blob {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
height: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1480px) {
|
||||
.cards-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.blob {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1297px) {
|
||||
.cards-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1210px) {
|
||||
h1 {
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1033px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
#features {
|
||||
padding: 20px;
|
||||
}
|
||||
header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
padding: 0;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.blob {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-bottom: 50px;
|
||||
height: fit-content;
|
||||
aspect-ratio: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.organic-btn-secondary {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
padding: 20px 50px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
<script>
|
||||
import { resolve } from '$app/paths';
|
||||
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
|
||||
import { t } from '$lib/translations';
|
||||
import { isMobile } from '$lib/utils/isMobile';
|
||||
|
||||
const backgroundImage = isMobile()
|
||||
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/hero-mobile.webp`
|
||||
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/hero.webp`;
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="hero"
|
||||
style={`background: url(${backgroundImage}) center/cover no-repeat`}
|
||||
>
|
||||
<div class="overlay">
|
||||
<h1>{$t('hero.title')}</h1>
|
||||
<p>{$t('hero.description')}</p>
|
||||
|
||||
<a class="organic-btn" href={resolve('/viewer')}>{$t('hero.cta')}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#hero {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
z-index: 1;
|
||||
padding-left: 100px;
|
||||
padding-top: 130px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(3.5rem, 4vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1350px) {
|
||||
h1 {
|
||||
font-size: clamp(3.3rem, 4vw, 3.3rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
h1 {
|
||||
font-size: clamp(3rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
h1 {
|
||||
font-size: clamp(3rem, 4vw, 3rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: clamp(2rem, 4vw, 2rem);
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#hero {
|
||||
background-size: contain !important;
|
||||
background-position: bottom !important;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100%;
|
||||
padding-top: 100px;
|
||||
padding-left: 0;
|
||||
height: 100vh;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||